0%

AMS启动一个应用程序首先要保证该应用程序的进程已经启动,如果应用程序进程还不存在,则请求 Zygote 进程启动进程。

从前一章内容可知,在Zygote 的Java 框架层会创建Server 端的Socket,用于等待AMS 请求Zygote 创建新的应用程序进程。Zygote 通过fork自身即可创建应用程序进程,这个进程会包含虚拟机实例,并且会创建Binder线程池和消息循环。

本章学习应用程序进程的启动,注意,是应用程序进程而不是应用程序的启动过程。

应用程序进程启动过程介绍

AMS发送启动应用程序进程请求过程的时序图

AMS 通过调用 startProcessLocked 向 Zygote 进程发送请求, 主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private final void startProcessLocked(ProcessRecord app, String hostingType,
String hostingNameStr, String abiOverride ,String entryPoint ,String [] entryPointArgs) {
...
//获取要创建的应用程序进程的用户 ID
int uid = app.uid; //1

int [] gids = null;
if (!app.isolated) {
//2 gids创建和赋值
if (ArrayUtils isEmpty(permGids)){
gids =new int[3];
}else {
gids = new int[permGids.leng + 3] ;
System.arraycopy(permGids, 0, gids, 3, permGids.length) ;
gids[O] = UserHandle.getSharedAppGid(UserHandle.getAppid(uid));
gids[1] = UserHandle.getCacheAppGid(UserHandle .getAppid(uid)) ;
gids[2] = UserHandle.getUserGid{UserHandle.getUserid(uid));
}

...
if (entryPoint = null) entryPoint = "android.app.ActivityThread";//3

...
//启动应用程序进程,这里省略不重要的参数
startResult = Process.start(entryPoint,xxx,uid, xxx,gids,xxx,xxx);

上述代码首先得到应用程序进程的用户ID,之后创建用户组ID 并且赋值,在注释3处判断 entryPoint 为null就赋值 “android.app.ActivityThread”,这个值就是应用程序进程主线程的类名。在最终调用 Process.start 创建进程时,会将进程的用户ID和用户组ID传入。

Process.start会将实现交给 ZygoteProcess(ZygoteProcess用于保持与Zygote进程的通信状态)的start 方法处理,其中首先会执行 openZygoteSocketifNeeded 方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
...
if (primaryZygoteState == null || primaryZygoteState.isClosed ()) {
try {
//与 Zygote 进程建立 Socket 连接
primaryZygoteState = ZygoteState.connect(mSocket);//1
} catch (IOException ioe} {
throw new ZygoteStartFailedEx ("Error connecting to primary zygote", ioe)
}
//连接 Zygote 主模式返回的 ZygoteState 是否与启动应用程序进程所需要的ABI匹配
if (primaryZygotestate.matches (abi)) {//2
return primaryZygoteState;
}

//如果不匹配,则尝试连接 Zygote 辅模式
if (secondaryZygoteState ==null || secondaryZygoteState.isClosed()) {
try {
secondaryZygoteState = ZygoteState.connect(mSecondarySocket);//3
} catch (IOException ioe) {
throw new ZygoteStartFailedEx ("Error connecting to secondary zygote", ioe) ;
}

//连接 Zygote 辅模式返回的 ZygoteState 是否与启动应用程序进程所需要的 ABI 匹配
if (secondaryZygoteState.matches(abi)) {//4
return secondaryZygoteState;
}
throw new ZygoteStartFailedEx ("Unsupported zygote ABI :" + abi);

前面章节提到Zygote的main方法中会创建name为 “zygote” 的server端Socket,注释1就是与Zygote进程建立Socket 连接(name为”zygote”),并得到 ZygoteState 对象primaryZygoteState;注释2处如果primaryZygoteState 与启动应用程序所需要的 ABI 不匹配,则在注释3处连接name为 “zygote_secondary” 的Socket。如果辅模式返回的 ZygoteState 与进程所需的ABI也不匹配,就抛出异常。

通过 openZygoteSocketifNeeded 获取ZygoteState后,将其作为参数调用 ZygoteProcess的 ZygoteSendArgsAndGetResult 方法 获取Process.ProcessStartResult。

Zygote 接收请求并创建应用程序进程

执行完以上步骤后,继续执行 argsForZygote 方法即可让Zygote 收到创建新的应用程序进程的请求。Zygote 接收请求并创建应用程序进程的时序图如下:

Zygote接收请求并创建应用程序进程的时序图

由前面章节我们知道,Zygote 中的Server Socket 的runSelectLoop 方法一直在等待 AMS请求创建新的应用程序进程,runSelectLoop 中是通过 runOnce 来创建进程的,runOnce最终调用到 Zygote 的 forkAndSpecialize 方法创建应用程序进程,并返回pid。forkAndSpecialize 通过fork 当前进程来创建一个子进程。此后,便会在应用程序进程中创建 Binder 线程池,*进一步通过反射 ActivityThread 类的main方法调用,令应用程序进程进入了ActivityThread 的main方法中。此时,应用程序进程创建完毕,并且运行了主线程管理类 ActivityThread *

如果pid为0,说明当前代码运行在新创建的子进程中。

Binder 线程池启动过程

程序中会检查代码,确保Binder线程池只会被启动一次。

消息循环创建过程

通过反射方式 invok 执行 ActivityThread 的main方法时,会执行一系列主线程的工作,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//frameworks/base/core/java/android/app/ActivityThread.java 

public static void main(String[] args) {
...
//创建主线程 Looper
Looper.prepareMainLooper(); //1
ActivityThread thread= new ActivityThread(); //2
thread.attach(false) ;
if (sMainThreadHandler == null ) {//3
//创建主线程 H 类
sMainThreadHandler = thread . getHandler ();//4
if (false) {
Looper.myLooper().setMessageLogging(new LogPrinter(Log.DEBUG, "ActivityThread");
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER) ;
//Looper 开始工作
Looper.loop (); //5
throw new RuntimeException("Main thread loop unexpectedly exited");
}

ActivityThread类用于管理当前应用程序进程的主线程,上述代码中创建了主线程的消息循环Looper,其中的内部类 H 继承 Handler,在注释 5 处调用Looper.loop ,即使得Looper 开始处理消息,可见,系统在应用程序进程启动完成后,就会创建消息循环。

1、对象的内存布局

点击看答案

分为3个区域:对象头、实例数据 以及 对齐填充

对象头:包括两部分信息,第一部分:对象自身运行时数据,如hashcode、GC年龄分代、锁状态标志位,官方称为”Mark Word”。第二部分:类型指针,虚拟机通过这个指针确定对象是哪个类的实例。

实例数据:对象真正存储的有效信息。

对齐填充:比如HotSpot vm 要求对象起始地址必须是8的整数倍。对齐填充不是必需的。

以下是句柄访问 时,内存布局关系图:

内存布局关系图

以上内容参考自: 对象创建与定位

2、垃圾收集算法

点击看答案
  • 标记-清除 算法。不足:1、标记清除效率不高。2、产生内存碎片。
  • 复制算法。为了解决标记清除的效率问题,将内存划分为大小相等的两块,每次使用一块。不足:可用内存缩小为原来一半。
  • 标记-整理。不足:复制存货对象耗时过多。
  • 分代收集算法。 新生代使用复制算法;老年代采用“标记-清除” 或者 “标记-整理”算法。

以上内容参考自: 垃圾回收算法

3、说说四大引用?强,软,弱,虚,并说明下合适GC

点击看答案

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。

软引用是用来描述一些有用但并不是必需的对象。对于软引用关联着的对象,只有在内存不足的时候 JVM 才会回收该对象。这一点可以很好地用来解决 OOM 的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。要注意的是,虚引用必须和引用队列关联使用.

4、Android中内存分配的执行流程

点击看答案

铺垫

Dalvik 虚拟机的Java堆的底层实现是一块共享匿名内存(Ashmem),并且将其抽象为C库的一个 mspace ,于是,Dalvik 虚拟机就可以利用 C 库里面的 dlmalloc 内存分配器来解决内存碎片问题(这是个成熟的内存分配器,可以很好地解决内存碎片的问题)

Android中内存分配的执行流程:

流程图如下:

dalvik虚拟机内存分配流程

  1. 尝试在Java堆上分配指定大小的内存,如果内存充足,就直接分配成功。(不改变java堆当前大小的前提下进行内存分配,属于轻量级的内存分配)
  2. 如果分配失败,就执行一次GC(如果此时有GC在运行,则等待这次GC执行完成),GC 时设置参数false标记不要回收软引用的对象。
  3. GC 完成后,再次尝试轻量级内存分配操作,如果内存充足,就分配成功了
  4. 如果上一步内存分配失败,就考虑将Java堆的当前大小设置为Dalvik 虚拟机启动时指定的Java堆最大值,再进行内存分配
  5. 如果内存充足,就完成内存的分配
  6. 如果上一步还是分配失败,就再次调用GC,并将参数标记为true,表示要回收软引用的对象
  7. GC完毕后,再次尝试分配,成功就返回;不成功也就抛出OOM了

以上内容参考自:老罗的博客

5、Android GC 流程

Art 虚拟机GC过程

点击看答案

Art的gc流程

由图可知,非并行GC的过程如下所示:

  1. 挂起所有的ART运行时线程。
  2. 调用子类实现的成员函数MarkingPhase执行GC标记阶段。
  3. 调用子类实现的成员函数ReclaimPhase执行GC回收阶段。
  4. 恢复第2步挂起的ART运行时线程。

并行GC的过程如下所示:

  1. 获取用于访问Java堆的锁。
  2. 调用子类实现的成员函数MarkingPhase执行GC并行标记阶段。
  3. 释放用于访问Java堆的锁。
  4. 挂起所有的ART运行时线程。
  5. 调用子类实现的成员函数HandleDirtyObjectsPhase处理在GC并行标记阶段被修改的对象。。
  6. 恢复第4步挂起的ART运行时线程。
  7. 重复第5到第7步,直到所有在GC并行阶段被修改的对象都处理完成。
  8. 获取用于访问Java堆的锁。
  9. 调用子类实现的成员函数ReclaimPhase执行GC回收阶段。
  10. 释放用于访问Java堆的锁。

从上面的分析就可以看出,并行GC和非并行GC的区别在于:

  1. 非并行GC的标记阶段和回收阶段是在挂住所有的ART运行时线程的前提下进行的,因此,只需要执行一次标记。
  2. 并行GC的标记阶段只锁住了Java堆,因此它不能阻止那些不是正在分配对象的ART运行时线程同时运行,而这些同进运行的ART运行时线程可能会引用了一些在之前的标记阶段没有被标记的对象。如果不对这些对象进行重新标记的话,那么就会导致它们被GC回收,造成错误。因此,与非并行GC相比,并行GC多了一个处理脏对象的阶段。所谓的脏对象就是我们前面说的在GC标记阶段同时运行的ART运行时线程访问或者修改过的对象。
  3. 并行GC并不是自始至终都是并行的,例如,处理脏对象的阶段就是需要挂起除GC线程以外的其它ART运行时线程,这样才可以保证标记阶段可以结束。

Dalvik 虚拟机垃圾收集过程

点击看答案

Dalvik 虚拟机使用 Mark-Sweep 算法来进行垃圾收集
Dalvik 执行GC 时会有一些选项:

  • isPartial,为true 时,表示仅仅回收Active 堆的垃圾;为false时,表示同时回收Active 堆和Zygote 堆的垃圾
  • isConcurrent:为true时,表示执行并行GC,false时,表示非并行GC
  • doPreserve:为true的时候,表示不回收软引用的对象;false的时候,表示回收软引用对象
    Dalvik在如下几种情况会触发GC:
  • 分配对象时,内存不足触发GC
  • 已经分配的内存达到一定阈值时触发GC
  • 调用 System.gc 、VMRuntime.gc 或者收到信号触发 GC
  • 准备抛出 OOM 前而最后进行的 GC
    GC线程在空闲达到一定时间后,会调用函数对Java堆进行裁剪,将一些没有用到的内存交还给内核。
    dalvik 的GC 流程(并行和非并行情况)如下图所示:
    dalvik虚拟机gc流程

哪些对象可以做 GC Root

  • 虚拟机栈引用的对象
  • 本地方法栈引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • Dalvik 虚拟机内部创建的部分异常对象
  • Dalvik 虚拟机内部创建的原子类
  • 注册在调试器的对象

以上内容参考自以上内容参考自 罗升阳的博客老罗的博客

6、如何理解Java类加载机制

1、mvc、mvp、mvvm

点击看答案

mvc

mvc示意图

特征:

  • view 接收用户的输入,并且可以通知Controller 更改Model 层数据,如果没有涉及逻辑,还可以直接通知Model 更改数据。
  • 需要ui变化,但是没有涉及业务数据,直接调用controller 即可(比如用户选择使用用户名/手机号登录)
  • model 数据变化时,若需要修改ui,需要通过Controller 来通知更新。

在Android中存在问题:

  • Activity 的职责不明,并且臃肿。Activity 除了要展示view (加载view,接收用户操作),还要承担部分逻辑处理(比如生命周期回调)

mvp

mvp示意图

特点:

  • View 层接收输入
  • Presenter 处理业务逻辑,View 与 Model 之间必须经过 Presenter

优点(相对MVC):

  • 避免了View、Model 直接联系,更加解耦,通过Presenter 实现两者之间的沟通
  • Activity 更加简明,仅仅负责UI上的工作

mvvm

mvvm示意图

可能主要关注view 通过databinding 与 viewmodel 相互绑定。

mvc 与 mvp 的区别

  • 在mvc 中 Activity 具有 view 的功能,并且兼任部分 controller 功能,而mvp 中Activity 是纯粹的view
  • mvc 中view 和model 可以直接沟通(没有逻辑情况下,view直接更改model,当然,有逻辑改动的情况下经过controller),而mvp中二者沟通必须经过presenter。

以上内容参考自carson_ho的博客

2、实现一个线程安全的单例模式

3、观察者模式和回调有什么区别?

Dalvik虚拟机

Dalvik虚拟机 简称 DVM,第10章提到,它不是一个 jvm,主要原因是它没有遵循jvm规范实现,二者的主要区别有:

  • 基于的架构不同

JVM 基于栈,DVM 基于寄存器。基于栈意味着需要去栈中读写数据,所需要的指令更多,并且速度更慢,对于性能优先的移动设备,显然是不合适的。

  • 执行的字节码不同

在Java SE 程序中,Java 类被编译成一个或者多个.class 文件,并被打包成 jar 文件,之后JVM 会通过相应的 .class 和 jar 文件获取相应的字节码;而DVM 会用dx工具将所有的 .class 文件转换为一个 .dex 文件,然后DVM 从该 .dex 文件中读取指令和数据。
.jar文件里面包含多个.class 文件,每个.class文件包含了该类的常量池、类信息、属性等,当JVM 加载.jar文件的时候,会加载里面所有的.class文件,JVM 这种加载方式很慢(首先就是很多io操作),对于内存有限的移动设备并不合适;而.dex文件将所有的.class里面所包含的信息全部整合到一块了,这样再加载就减少了I/O操作,加快查找速度;并且,相对Android而言,.class中有许多冗余信息,dex工具会去掉冗余信息。

DVM与JVM执行的字节码不同

  • DVM 允许在有限的内存中同时运行多个进程

在Android中,每个应用都运行在一个DVM中,每个DVM 实例都运行在一个独立的进程中,这样,某一个虚拟机崩溃的时候不会导致其他app也关闭。

  • DVM 由Zygote创建和初始化

在第2章有介绍Zygote,它是一个DVM进程,当系统需要创建一个应用程序时,Zygote就会fork自身,快速地创建和初始化一个DVM实例。对于一些只读的库,所有的DVM实例都会和Zygote共享一块内存区域,节省内存开销。

  • DVM 有共享机制

DVM 的共享机制可以使不同应用之间在运行时可以共享相同的类,这带来更高的效率;而JVM不具有这种机制,不同的程序,打包以后的程序都是彼此独立的,即便它们在包里使用了同样的类,运行时也是单独加载和运行的。

DVM 运行时堆

DVM运行时堆使用标记-清除算法进行GC,它由两个Space以及多个辅助数据结构组成,两个Space分别是:Zygote Space(Zygote Heap) 和 Allocation Space(Active Heap),前者用于管理Zygote进程在启动过程中预加载和创建的对象,并且Zygote Space不会触发GC,Zygote 进程和应用进程之间会共享Zygote Space。在Zygote 进程fork第一个子进程前,会把Zygote Space 分为两部分,原来已经被使用的部分对仍旧叫做Zygote Space,而未使用的那部分堆叫做 Allocation Space ,以后的对象都会在 Allocation Space上进行分配和释放。

ART虚拟机

Android 4.4 的时候发布了ART虚拟机,但是4.4版本默认还是使用DVM,Android 5.0 及以后版本默认采用ART,从此,DVM退出历史舞台。

ART 与 Dalvik 区别

ART 与 Dalvik 的区别主要有4点:

  • DVM 是为32位CPU涉及的,而ART支持64位并且兼容32位 CPU。
  • ART 对垃圾回收机制进行了改进,将 GC 暂停由2次减少为1次,并且频繁执行并行垃圾收集。
  • ART 的运行时堆空间划分和DVM不同。
  • DVM中应用每次运行时,字节码通过 JIT 编译器编译为机器码,使得应用程序运行效率低下;而在ART中,系统在安装应用程序时会进行一次AOT(ahead of time compilation,预编译),将字节码预先编译成机器码并存储在本地,这样应用程序每次运行时就不需要执行编译了。

ART也有两个主要缺点:一是AOT使应用安装时间变长,二是预编译的机器码占用的存储空间比较大。为了解决上面的缺点,Android 7.0 版本在ART中加入了JIT编译器,作为AOT的补充:在安装应用时不会将字节码全部编译成机器码,而是在运行中将热点代码编译器机器码,以达到缩短应用安装时间并节省存储空间。

更详细的内容,可以参考官网上的描述

ART 运行时堆

与DVM 的GC不同的是,ART采用多种垃圾收集方案,每个方案会运行不同的垃圾收集器,默认是采用 CMS(Concurrent Mark-Sweep)方案,主要有sticky-CMS 和 partial-CMS,不同的CMS方案,ART运行时堆得空间划分也不同,默认由4个Space和多个辅助结构组成,采用标记-清除算法时,两种虚拟机运行时堆对比图如下:

两种虚拟机运行时堆对比

由图可以看到ART的4个Space,其中,Zygote Space 、Allocation Space 和 DVM 中的作用一样,ImageSpace 用来存放一些预加载类,Large Object Space 用来分配一些大对象。其中Zygote Space 与 Image Space 是进程共享的。

ART 的GC 日志

  1. GC Reason
    ART 虚拟机GC日志中会包含产生这次GC原因,主要会有:
  • Concurrent: 并发GC,在后台线程运行GC,不会使App的线程暂停,不会阻止内存分配。
  • Alloc: 当堆内存已满,App尝试分配内存而引起的GC,这个GC发生在正在分配内存的线程中。
  • Explicit: App显式请求垃圾收集,比如 System.gc()。
  • NativeAlloc: Native 内存分配时触发的GC。
  1. 垃圾收集器名称
    ART 虚拟机GC日志中会包含所使用的收集器名称,主要会有:
  • Concurrent Mark Sweep(CMS): 它是以最短收集暂停时间为目标的收集器,采用标记-清除算法,能释放除了Image Space外的所有空间
  • Concurrent Partial Mark Sweep: 能释放除了Image Space 和 Zygote space 意外的所有空间
  • Concurrent Sticky Mark Sweep: 粘性收集器,基于分代的垃圾收集思想,只能释放自上次GC以来分配的对象,这个收集器扫描比较频繁,因为它很快并有很短的暂停时间。
  • Marksweep + Semispace:非并发GC,复制GC用于堆转换以及堆碎片整理。

DVM 和 ART 的诞生

它们是从Zygote进程诞生的,这样,Zygote进程就持有了DVM或者ART的实例,此后,Zygote进程每次fork自身创建新的应用进程时,应用程序进程也就得到了 DVM 或者ART 的实例,即每个应用进程都有一个单独的虚拟机实例。这样做的好处是,无需在每次启动应用程序进程时都要创建DVM或者ART,从而加快了应用程序进程的启动速度

init 进程启动过程

init进程是Android系统中用户空间的第一个进程,进程号为1,它被赋予了很多重要职责,比如创建 Zygote 和属性服务等。

引入init进程

了解Android系统启动流程的前几步,可以有助于引入init进程:

  1. 启动电源以及系统启动。当电源按下时,引导芯片代码从预定义的地方(固化在ROM中)开始执行,加载引导程序到RAM中,然后执行。
  2. 引导程序BootLoader。它是Android操作系统开始运行前的一个小程序,主要作用是把系统OS拉起来。
  3. Linux 内核启动。linux内核启动完成系统设置后,它首先在系统文件中寻找init.rc,并启动init进程。
  4. init进程启动。init进程的工作比较多,主要用于初始化、启动属性服务 以及 启动Zygote进程。

init进程入口

在Linux内核加载完成后,首先在系统文件中查找init.rc,并启动init进程。init的main函数中做了很多事情,我们主要了解以下几点:

  • 调用property_init 函数对属性初始化,调用 start_property_service函数启动属性服务。
  • 调用 signal_handler_init 设置子进程信号处理函数,主要用于防止init进程的子进程成为僵尸进程。为了防止僵尸进程的出现,系统会在子进程暂停和终止的时候发出 SIGCHLD信号,signal_handler_init就是用来接收这个信号的。

僵尸进程:在linux中,父进程使用fork创建子线程,在子线程终止后,如果父进程并不知道子线程终止了,那么还会在系统进程表为它保留一定的信息。僵尸进程主要危害就是占用系统进程表,可能导致系统进程表被耗尽而无法创建新的进程。假设init的子进程Zygote终止了,signal_handler_init 函数会找到Zygote进程并移除所有Zygote进程信息,再重启Zygote。

解析 init.rc

init.rc 是一个配置文,是由Android 初始化语言编写的脚本,为了分析如何创建Zygote,我们主要查看其中的Service类型语句,在Android 8.0 及以后,每个Service 都对应一个rc 文件。init.rc 中定义Service 的格式如下:

service [ ] * //<service 的名字〉〈执行程序路径〉〈传递参数〉

举个栗子,zygote的Service配置文件就是:

1
2
3
4
5
service zygote /system/bin/app_process64 -Xzygote /system/bin -- zygote --startsystem- server 
class main
priority -20
user root
group root readproc

来大概分析代码意思,Service用于通知 init 进程创建名为 zygote 的进程,这个进程执行程序的路径为 /system/bin/app_process64,其中 class main指的是Zygote 的classname 为 main。后面的代码是要传给 app_process64的参数。

解析Service类型语句

init.rc中Service类型语句使用ServiceParser类来解析,它将init.rc中的service解析生成的Service对象加入到 Service 链表中。

init 启动Zygote

我们来了解init是如何启动Service的,这里主要讲解 Zygote这个Service。前面提到,在Zygote的启动脚本中描述Zygote的classname为main,在init.rc中会遍历 解析Service类型语句 过程生成的Service链表,找到 classname 为main的Zygote,如果Zygote Service 已经运行,则不再启动,否则就调用fork函数创建子进程,并在子进程中调用Service的main函数,Zygote的main函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
int main (int argc, char* const argv []) 
{
if (zygote) {
runtime.start ("com.android.internal.os.Zygoteinit”, args, zygote) ; //1
} else if (className) {
runtime .start (” com android nternal.os.Runtimeinit ”, args zygote) ;
} else{
...
}
}

...

从注释1处可以看到调用runtime的start函数启动了 Zygote 。

属性服务

Windows上有注册表管理器以键值对的形式记录用户软件的一些使用信息,即使系统或者软件重启,其还是能够根据之前注册表中的记录进行相应初始化工作。Android中也提供了类似机制,叫做属性服务。init 进程启动时会启动属性服务,并为其分配内存存储这些属性,如果需要这些属性直接读取即可。启动属性服务的主要代码如下:

1
2
3
4
5
...
property_set_fd =create_socket(PROP_SERVICE_NAME ,SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK,0666, 0, 0, NULL); //1
...
listen(property_set_fd, 8); //2
register_epoll_handler(property_set_fd , handle_property_set_fd) ; //3

由代码可知,首先创建非阻塞的Socket,并对property_set_fd 监听,这样创建的Socket 就成为server,也就是属性服务;listener的参数意味着可以同时为8个试图设置属性的用户服务。最后使用epoll 来监听property_set_fd :当property_set_fd中有数据到来时,init进程将调用handle_property_set_fd函数处理。

epoll是linux下多路复用I/O接口,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU的利用率。

对属性进行修改时,首先判断属性的合法性,之后查找属性,如果属性存在就更新属性值,否则添加属性。

init进程启动总结

主要做了3件事:1、创建和挂载启动所需文件目录 2、启动服务属性 3、解析init.rc ,并启动 Zygote 进程。

Zygote进程启动过程

在Android系统中,DVM(或者ART)、应用程序进程、SystemServer进程等都是由Zygote进程创建的。由于Zygote在启动时会创建 DVM(或者ART),因此其fork的应用程序进程和SystemServer进程可以在内部获取一个 DVM 或者 ART。

在Zygote 中通过 JNI 调用 ZygoteInit(ZygoteInit是由Java编写的)的 main 方法后,Zygote 便进入了Java 框架层,此前没有任何代码进入Java框架层的。即Zygote开创了Java框架层。

ZygoteInit的main方法中主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//frameworks/base/core/java/com/android/internal/os/Zygotelnit.java 

...
//创建一个 Server 端的 Socket socketName 的值为"zygote"
zygoteServer.registerServerSocket(socketName);

...
//预加载类和资源
preload(b otT U.ngsTraceLog);

...
if (startSystemServer) {
//启动 SystemServer 进程
startSystermServer(abiList, socketName, zygoteServer);
}
//等待 AMS 请求
zygoteServer.runSelectLoop(abiList);

由代码可知,通过 registerServerSocket 方法创建一个Server端的Socket ,这个名为 “zygote” 的Socket 用于等待 ActivityManagerService 请求 Zygote 来创建新的应用程序进程。总结一下,ZygoteInit 的main方法总共做了4件事:

  1. 创建一个Server端的Socket
  2. 预加载类和资源
  3. 启动 SystemServer 进程
  4. 等待 AMS 请求创建新的应用程序进程

Zygote 进程启动总结

Zygote进程启动共做了以下几件事:

  1. 创建AppRuntime ,并调用其start方法,启动Zygote 进程
  2. 创建java虚拟机并未Java虚拟机注册 JNI 方法
  3. 通过 JNI 调用ZygoteInit 的main函数进入Zygote 的Java 框架层
  4. 通过 registerServerSocket 创建服务端 Socket ,循环等待AMS 请求来创建新的应用程序进程。
  5. 启动 SystemServer

SystemServer 处理过程

SystemServer 进程主要用于创建系统服务,如 AMS、WMS、PMS 。在 ZygoteInit.java 中启动了 SystemServer,代码如下:

1
2
3
4
5
6
7
8
9
10
11
private static boolean startSystemServer(String abiList, String socketName) throws MethodAndArgsCaller, RuntimeException{
//当前运行在 SystemServer 进程中
if (p == 0) {
if (hasSecondZygote(abiList)) {
waitForSecondaryZygote (socketName );
}
//关闭 Zygote 进程创建的 Socket
zygoteServer . closeServerSocket() ; //1
handleSystemServerProcess(parsedArgs) ; //2
}
return true;

由以上代码可知,SystemServer 进程复制了 Zygote 进程的地址空间,因此也会得到Zygote 创建的 server Socket,这个socket对SystemServer 没有用处,因此在代码 1处将其关闭,之后通过 handleSystemServerProcess 方法启动 SystemServer 进程。接下来,在ZygoteInit中创建了 PathClassLoader,并且通过native 方法 ZygoteInit.nativeZygotelnit 启动了Binder线程池,之后进入 SystemServer 的main 方法。

SystemServer 的run 方法中用 SystemServiceManager 启动了 ActivityManagerService、 PackageManagerService 等服务。在PackageManagerService 创建完成后,将其注册到 ServiceManager 中,ServiceManager 用于管理系统中各种Service,用于系统C/S 架构中的Binder 通信机制:Client端要使用某个Service,需要先到 ServiceManager 中查询Service 的相关信息,然后与Service 所在的Server 进程建立通信通路,这样Client就可以使用Service 了

SystemServer 进程总结

SystemServer 进程被创建后,主要工作:

  1. 启动Binder 线程池,这样可以与其他进程通信。
  2. 创建 SystemServiceManager,创建和管理系统服务
  3. 启动各种服务

Launcher 启动过程

系统启动的最后一步是启动一个应用程序来显示系统中已经安装的应用程序,即 Launcher。Launcher 在启动过程中请求 PackageManagerService 获取系统中已经安装的应用程序信息,并且将信息封装成一个个快捷图标列表显示在系统屏幕上,用户就可以通过点击图标来启动相应的应用程序了。Launcher 的启动过程时序图如下:

Launcher的启动过程时序图

启动Launcher的入口方法为AMS 的systemReady 方法,该方法在SystemServer 的startOtherService中被调用,之后调用 ActivityStack 的resumeTopActivityUncheckedLocked 方法,ActivityStack 用于描述Activity 栈的。最终调用到 AMS 的 startHomeActivityLocked 方法,在该方法中,创建了 Launcher 启动所需的 Intent,并且在一系列判断后,将该Intent 的 Action 设置为 Intent.ACTION_MAIN 、Category 为 Intent.CATEGORY_HOME,并且在 Launcher 的AndroidManifest 中也是这么配置的。

经过以上操作,com.android.launcher3.Launcher 的Activity 就成为了主 Activity,此时,如果Launcher 还未启动,AMS 就会调用 ActivityStarter 的 startHomeActivityLocked 来启动 Launcher 。

Android系统启动流程

结合前面几节内容,可以总结出Android系统启动流程:

  1. 启动电源以及系统启动

    按下电源键,从预定义地方开始执行,加载BootLoader 到RAM。

  2. 引导程序BootLoader

    主要作用是拉起系统OS。

  3. Linux 内核启动

    内核启动,设置缓存、计划表、加载驱动等,之后寻找 init.rc 文件,并启动init 进程。

  4. init 进程启动

    初始化和启动属性服务,并启动 Zygote 进程。

  5. Zygote进程启动

    创建Java虚拟机,并未Java虚拟机注册 JNI 方法,创建服务端 Socket 循环等待AMS 请求来创建新的应用程序进程

  6. SystemServer 进程启动

    启动Binder 线程池 和 SystemServiceManager ,并启动各种服务。

  7. Launcher启动

    SystemServer 进程启动的 AMS 会启动 Launcher,Launcher 会将已安装的应用显示到界面上。

用图表示就是:

Android系统启动流程图

1、TCP/IP 三次握手(为什么3次,能否2次或者4次?),四次挥手(为什么4次,3次可以吗)

首先要会画 三次握手和四次挥手 的图。很重要,画了两次错了

3次握手的示意图如下:

三次握手

三次握手过程:TCP发起的一方A,由操作系统生成一个序列号 n,以这个n为原点,对自己每个将要发送的数据进行编号,连接的另一方 B 会对A每次发送的数据进行确认,如果A收到B的确认编号2001,则可以认为 n~2000编号已经全部到达B。

为什么三次握手

  1. 首先,三次握手证明双方收发能力正常
  2. 其次,如果只需要两次握手,那第二次(从server发到client端)之后server就分配资源了,但是如果第二次丢失了,那就造成资源浪费
  3. 还有,如果client没有收到确认,就会再次发送握手连接,如果服务端先接到了第二个消息,然后发送返回确认,双方通信完毕,关闭。这时候第一个请求到了服务端,服务端确认,然而客户端已经关闭了,不理睬,这时候服务端只能一直等。
  4. 还有,握手就确认了双方的序列号规则

可以不握手吗?

tcp实现可靠通信与 报文序号和确认号(ack) 是分不开的,我们知道握手是干的啥,其中就有告知初始序号的功能,所以不能不握手。

4次挥手示意图:

四次挥手

由上图看出,由client端发起挥手,server给client的2次回复中,一次是确认信息ack,一次是数据的传送,那可不可以只要3次挥手呢?答案是可能的,TCP是全双工的,Client在没有数据需要发送给Server后,就发送FIN告知server,然后终止对server发送消息,但这个时候server还以发送数据给client,这时候需要4次;但是如果server收到client的FIN后再也没有数据要发给client了,那么 对client的ack包和server自己的FIN包就能合并成一个包发送过去,四次挥手变成3次。

了解完挥手完成后,可能还想更深入了解 TIME_WAIT,可以查看之前写的这篇

2、延伸-握手可能有什么安全问题

首先,发起请求时候的序列是随机的,为什么是随机的呢?想想如果是固定的从0开始,那么客户端发送多次握手(重传),那么,当新连接建立之后,如果上一次的请求再过来了,序列还是 0 ,那就乱了。相反,如果序列不固定,那么上一次的过来,我只要和新连接这里对比下 序列号 就知道是否合法了。

  • client第一个 SYN 包丢失,这个无所谓,后续触发重传机制,隔5s,24s等
  • server 收到 SYN ,但是回复的 SYN,ACK 丢失,也会触发重传,可能依次隔 3s,6s,12s 等
  • client 最后依次发送的 ACK 丢了,server 因为没收到 ACK 而实现重传,但是client 已经进入 ESTABLISHED 状态了。因为多数情况下,client 发送完最后的 ACK之后,就认为连接建立,开始发送数据。现实中的情况是,在这种情况下 server 收到 client 的数据会进入 ESTABLISH 状态,并会认为数据有效。
  • 如果client 故意不发最后一次 ACK,那么服务端处于半连接状态,如果这种半连接很多的话,server 端很有压力,因为需要一直重传

以上内容参考自知乎上的文章知乎上的文章1知乎-泪花的回答

3、延伸-三次握手可以携带数据吗?

第一次、第二次不可以,第三次可以。想想为什么?

  • 假如第一次允许带数据,那么攻击者只需要在第一次SYN中放入大量数据,根本不考虑服务器接收能力,这会让server 花费很长时间和很多空间来处理这些报文
  • 并且,第一次不知道server 是否有接收能力;如果放第二次,server 也不知道clien 是否有接收能力。

以上内容参考自知乎上的文章知乎上的文章1知乎-泪花的回答

4、引申——为什么要等待2MSL?

原因有二:保证TCP协议的全双工连接能够可靠关闭 以及 保证这次连接的重复数据段从网络中消失

第一点:如果主机1直接CLOSED了,那么由于IP协议的不可靠性或者是其它网络原因,导致主机2没有收到主机1最后回复的ACK。那么主机2就会在超时之后继续发送FIN,此时由于主机1已经CLOSED了,就找不到与重发的FIN对应的连接。所以,主机1不是直接进入CLOSED,而是要保持TIME_WAIT,当再次收到FIN的时候,能够保证对方收到ACK,最后正确的关闭连接。

第二点:如果主机1直接CLOSED,然后又再向主机2发起一个新连接,我们不能保证这个新连接与刚关闭的连接的端口号是不同的。也就是说有可能新连接和老连接的端口号是相同的。一般来说不会发生什么问题,但是还是有特殊情况出现:假设新连接和已经关闭的老连接端口号是一样的,如果前一次连接的某些数据仍然滞留在网络中,这些延迟数据在建立新连接之后才到达主机2,由于新连接和老连接的端口号是一样的,TCP协议就认为那个延迟的数据是属于新连接的,这样就和真正的新连接的数据包发生混淆了。所以TCP连接还要在TIME_WAIT状态等待2倍MSL,这样可以保证本次连接的所有数据都从网络中消失。

5、IP地址分类(A/B/C/D类)

IP地址由四段组成,每个字段是一个字节,即4个字节、 每个字节有8位,最大值是255,IP地址 = 网络号+主机号。分为A、B、C三类及特殊地址D、E。全0和全1的都保留不用。

ip地址分类

由图可知:

A类:前1个字节(8位)为网络号,后3个字节(24位)为主机号。A类第1位必须是0
B类:前2个字节(16位)为网络号,后2个字节(16位)为主机号。B类前两位固定为10
C类:前3个字节(24位)为网络号,后1个字节(8位)为主机号。C类前3位固定为110
D类: 是多播地址,一般用于多路广播用户。该类IP地址的最前面为1110
E类: 是保留地址。该类IP地址的最前面为1111

6、TCP与UDP区别

  1. UDP发送数据前无需建立连接,TCP需要握手建立连接。
  2. UDP尽最大努力交付,不保证可靠交付;TCP提供可靠交付。
  3. UDP面向报文,对应用程序交下来的报文,在添加首部之后就交付给Ip层,不会对报文拆分;TCP面向字节流,会对应用程序的报文进行拆分。
  4. UDP支持一对一、一对多、多对一和多对一的通信;TCP全双工通信,并且只能点对点通信,允许通信双方在任何时候均能通信。
  5. UDP首部开销小,TCP开销大(UDP 8个字节,TCP20个字节)。
  6. UDP没有拥塞控制,在网络条件差的情况下发送端也不会降低发送速率;TCP有拥塞控制,慢开始避免拥塞,快重传、快恢复。
  7. TCP 通过滑动窗口实现流量控制,UDP 无。

需要参考知乎上的讲解)重写一下

7、引申-TCP流量控制

如果发送方把数据发送得过快,接收方可能会来不及接收,这就会造成数据的丢失。所谓流量控制就是让发送方的发送速率不要太快,要让接收方来得及接收。

利用滑动窗口机制可以很方便地在TCP连接上实现对发送方的流量控制。

设A向B发送数据。在连接建立时,B告诉了A:“我的接收窗口是 rwnd = 400 ”(这里的 rwnd 表示 receiver window) 。因此,发送方的发送窗口不能超过接收方给出的接收窗口的数值。请注意,TCP的窗口单位是字节,不是报文段。假设每一个报文段为100字节长,而数据报文段序号的初始值设为1。大写ACK表示首部中的确认位ACK,小写ack表示确认字段的值ack。

tcp流量控制

从图中可以看出,B进行了三次流量控制。第一次把窗口减少到 rwnd = 300 ,第二次又减到了 rwnd = 100 ,最后减到 rwnd = 0 ,即不允许发送方再发送数据了。这种使发送方暂停发送的状态将持续到主机B重新发出一个新的窗口值为止。B向A发送的三个报文段都设置了 ACK = 1 ,只有在ACK=1时确认号字段才有意义。

TCP为每一个连接设有一个持续计时器(persistence timer)。只要TCP连接的一方收到对方的零窗口通知,就启动持续计时器。若持续计时器设置的时间到期,就发送一个零窗口控测报文段(携1字节的数据),那么收到这个报文段的一方就重新设置持续计时器。

8、引申-快重传和快恢复

  • 快重传算法首先要求接收方每收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时才进行捎带确认。

快重传

  • 快恢复:当发送方连续收到三个重复确认,就开始执行拥塞避免算法,缩小拥塞窗口,随后再使拥塞窗口缓慢地线性增大。

9、Http和Https的区别?

Https是ssl加密传输,Http是明文传输
Https是使用端口443,而Http使用80
HttpsSSL+HTTP协议构建的可进行加密传输、身份认证的网络协议要比Http协议安全
Https协议需要到CA申请证书

10、中间人攻击

带着下列问题去了解Https 安全性:

  • 为什么用了https 就是安全的?用了https就一定安全吗?
  • https 的底层原理如何?
点击看答案

https的原理

https的整体过程分为证书验证和数据传输阶段,具体交互如下图:

https整体过程

所以证书验证阶段流程就是:

  1. 客户端发起https 请求
  2. 服务端返回https 证书
  3. 客户端验证证书是否合法

数据传输阶段流程就是:

  1. 证书合法后,在本地生成随机数
  2. 通过公钥加密随机数,并把加密后的随机数传输给服务端
  3. 服务端通过私钥对随机数解密
  4. 服务端通过客户端传入的随机数构造对称加密算法,对返回结果内容进行加密后传输

那我们再引申一下,为什么后来要使用对称加密传输数据。这是因为:

  • 非对称加密的效率低
  • https 上述场景中,只有服务端保存了私钥,一对公私钥只能单向加解密,而对称加密可以实现双向的加解密

中间人攻击

了解以上原理之后,中间人攻击的具体流程如下:

中间人攻击流程图

主要包括以下步骤:

  1. 本地请求被劫持(如dns劫持等),所有请求均发送到中间人的服务器
  2. 中间人返回自己自己的证书
  3. 客户端创建随机数并通过中间人的证书公钥加密,之后凭随机数构造对称加密对传输内容加密
  4. 中间人用私钥解密随机数,此时中间人拥有客户端的随机数,所以可以通过对称加密算法解密内容
  5. 中间人再以客户端的请求内容向正规服务器发送请求,此时中间人发起的请求就是正规请求了,数据也是正规数据,所以合法,正规服务器会通过建立的安全通道返回加密数据
  6. 此后流程都是合法化的了,中间人只是做了个转发而已

以上,就是中间人攻击的流程,我们日常使用的fiddler 代理,其实就很好地模拟了中间人攻击。因为我们使用fiddler 代理https 的时候,需要本地信任fiddler 的证书,其实就是中间人返回自己的证书过程,中间人攻击关键也是这一步。

一般客户端会验证证书的合法性,所以中间人的证书没有这么容易被信任,中间人攻击的难点也在这。

以上内容参考自知乎-Java面试那些事儿

11、TCP/IP 流量控制,拥塞避免,可以结合这篇文章

点击看答案

HTTP是一种无状态的协议,为了分辨链接是谁发起的,Session和Cookie就是为解决这个问题而提出来的两个机制。
Cookies是服务器下发,存储到本地机器上的一段文本,并随每一个请求发回服务器,是在客户端保持状态的方案。
浏览器第一次发送请求时,服务器自动生成了一HashTable和一个Session ID来唯一标识这个HashTable,并将其通过响应发送回浏览器。Session的实现方式和Cookie有一定关系,一般会把session id存在Cookie中(也可以依赖于url重写),每次访问的时候将Session id带过去,服务器就根据id查找hashtable就可以识别当前用户状态了。
所以可以总结,cookie存在本地,session存在服务器;父路径不能享用子路径的cookies,而同一个session的窗口可以共享session;cookie没有session可靠。

13、在浏览器中输入www.baidu.com后执行的全部过程

点击看答案
  1. 客户端浏览器通过DNS解析到www.baidu.com的IP地址220.181.27.48,通过这个IP地址找到客户端到服务器的路径。客户端浏览器发起一个HTTP会话到220.161.27.48,然后通过TCP进行封装数据包,输入到网络层。

  2. 在客户端的传输层,把HTTP会话请求分成报文段,添加源和目的端口,如服务器使用80端口监听客户端的请求,客户端由系统随机选择一个端口如5000,与服务器进行交换,服务器把相应的请求返回给客户端的5000端口。然后使用IP层的IP地址查找目的端。

  3. 客户端的网络层不用关系应用层或者传输层的东西,主要做的是通过查找路由表确定如何到达服务器,期间可能经过多个路由器,这些都是由路由器来完成的工作,我不作过多的描述,无非就是通过查找路由表决定通过那个路径到达服务器。

  4. 客户端的链路层,包通过链路层发送到路由器,通过邻居协议查找给定IP地址的MAC地址,然后发送ARP请求查找目的地址,如果得到回应后就可以使用ARP的请求应答交换的IP数据包现在就可以传输了,然后发送IP数据包到达服务器的地址。

14、HTTP中,POST与GET的区别

点击看答案
  1. Get是从服务器上获取数据,Post是向服务器传送数据。

  2. Get是把参数数据队列加到提交表单的Action属性所指向的URL中,值和表单内各个字段一一对应,在URL中科院看到。

  3. Get传送的数据量小,不能大于2KB;post传送的数据量较大,一般被默认为不受限制。

  4. 根据HTTP规范,GET用于信息获取,而且应该是安全的和幂等的。

15、http2 与http1 的区别

点击看答案

总结一下:1)、新的二进制格式 2)、多路复用 3)、header 压缩 4)、采用新的压缩算法 5)、server push 6)、更安全的SSL

  • 新的二进制格式

解释: http1 设计的时候是基于文本,http2 基于二进制格式。http2 的格式定义更接近tcp层的方式,length 定义了整个frame 的开始到结束,type 定义了frame 的类型,stream id 用作流控制。如下图所示:

http2格式变化

虽然看上去写一个是和http1完全不同了,实际上并没有改变http1的语义,只是把http1的header 和 body 用frame 重新封装了一层而已。他们的对应关系如下:

http2与http1对应关系

上述http1的首部信息会封装到 Header 的 frame,而相应的 body 封装到 Data frame。

  • 多路复用

解释:多路复用允许同时通过单一的 HTTP/2 连接发起多重的请求-响应消息。一个request对应一个 stream 并分配一个id,这样一个连接上可以有多个stream,每个stream 的frame 可以随机地混杂在一起,接收方可以根据stream id 将各个frame 再归属到不同的request 里面。

http2连接共享

http 性能优化的关键并不在于高带宽,而是低延迟,tcp 连接会随着时间进行自我“调谐”,起初会限制最大速度,如果传输成功,会随着时间提高传输的速度,这种调谐称为“慢启动”。http2 通过让所有数据流共用同一个连接,可以更高效地使用TCP。

客户端依据域名来向服务器建立连接,一般pc端浏览器会针对单个域名的server 同时建立 6~8 个连接,连接无法复用会导致请求要经历三次握手和慢启动。在http1中,假如有5个连接同时发出,在第一个请求没有收到回复之前,后续从应用层发出的请求只能排队,请求2,3,4,5只能等请求1的response 回来之后才能逐个发出。网络通畅的时候性能影响不大,一旦请求1的request 因为什么原因没有抵达服务器,影响到后续所有的请求,问题就比较严重了。

  • header 压缩

解释:http2针对header 采用新的压缩方式,高效地压缩算法可以很大地压缩header。并且,通讯双方各自缓存一份 header fields 表,避免重复header 的传输。

  • 采用新的压缩算法

解释:http2在采用gzip之外的新的压缩算法,更加安全。

  • server push

解释:http2 能通过push的方式将客户端需要的内容预先推送过去。

  • 更安全的SSL

解释:http2.0 对 tls 的安全性做了进一步加强。

> 以上内容参考自[知乎上的回答](https://www.zhihu.com/question/34074946)

16、网络状态码?以 2、3、4、5 开头的代表什么意思。

点击看答案

1xx-临时响应

100: 继续。请求者应当继续提出请求
101: 切换协议。请求者要求服务器切换协议,服务器确认并准备切换

2xx-成功

200: 成功。服务器成功处理请求
201: 已创建。请求成功并且服务器创建了新的资源
202: 已接受。 服务器接受请求,但是尚未处理
203: 非授权信息。服务器成功处理请求,但返回的信息可能来自另一来源
204: 无内容。服务器成功处理请求,但是没有返回任何内容
205: 重置内容。服务器成功处理请求,但是没有返回任何内容
206: 部分内容。服务器成功处理了部分GET请求

3xx-重定向

300: 多种选择。服务器根据请求者选择一项操作,火提供操作列表供请求者选择
301: 永久移动。请求的资源永久移动到新位置了,返回此响应时会自动转到新位置
302: 临时移动。从不同位置响应请求,但是后续还是以原来位置继续请求
303: 查看其它位置。
304: 未修改。自上次请求后,请求的网页未修改过,此时不会返回网页内容
305: 使用代理。请求者只能使用代理访问请求的网页。

4xx-请求错误

400: 错误请求。服务器不理解请求语法
401: 未授权。请求要求身份验证
403: 进制。服务器拒绝请求
404: 未找到。服务器找不到请求的网页
405: 方法禁用。禁用请求中指定的方法
408: 请求超时。服务器等候请求时发生超时
410: 已删除。请求的资源已永久删除
413: 请求实体过大。请求实体过大,服务器无法响应
414: 请求的URI过长。请求的URI(通常为网址)过长,无法处理
415: 不支持的媒体类型。

5xx-服务器错误

500: 服务器内部错误。服务器遇到错误,无法完成请求
501: 尚未实施。服务器不具备完成请求的功能
502: 错误网关。
503: 服务不可用。暂时地维护或者超载了
504: 网关超时。服务器作为网关或者代理,但是没有及时从上游服务器收到请求
505: HTTP版本不支持。服务器不支持请求中所用的HTTP协议版本

以上内容参考自站长工具

17、HttpDNS

点击看答案

当前 DNS 系统存在的问题

  • 域名缓存。运营商的 LocalDNS 会缓存域名解析的结果,而不是向权威域名发起递归。运营商缓存dns就可以实现把dns解析结果指向广告
  • 解析转发。运营商自身不进行域名递归解析,而是把它发给其它运营商。一些小运营商就是这样做的。这样,比如说访问的是qq.com,则腾讯的权威dns收到的域名解析请求的来源就变成了其它运营商,最终导致用户流量被导向了错误的IDC,比如明明是广东的用户,被导向去了北京的服务器。
  • NAT ,网络地址转换。运营商LocalDNS出口根据权威DNS目标ip进行NAT,导致腾讯权威dns无法正确识别运营商的 LocalDNS 的ip,引发域名解析错误,流量跨网等

解决方案

  • 使用 114 等中立DNS 服务器

  • 使用HttpDNS,HttpDNS 的基本原理如下:

HttpDNS的原理

注意:HttpDNS主要用于客户端

HttpDNS 的使用过程主要有两步:

  1. 客户端直接访问HttpDNS 接口,获取Host对应的IP。
  2. 客户端通过IP,向后台发送请求。以Http为例,通过在Header中指定host 字段,向HttpDNS 返回的IP 发送标准的Http请求即可。

以上内容参考自鹅厂网事上的博客

18、https的证书验证

点击看答案

证书是一级一级的验证的,验证当前的证书时,首先拿上一级的证书的公钥,用上一级的公钥来验证当前证书的签名,验证成功就通过,不成功就报错了。然后一级一级上去,顶级。因为本地(浏览器和操作系统)一般存有信任的根证书,顶级证书一般都能通过本地证书验证。

以上内容可以参考知乎上的内容

19、https的攻击方式

点击看答案

1、中间人攻击。类似Fiddler 这种方式,前提是黑客通过某种特殊手段在被攻击者的手机上安装了根证书
2、Https协议降级攻击。比如说client端仅仅支持某个有漏洞的ssl版本(比如SSLv3.0),那么服务端只能接收或者直接拒绝。如果接受的话,攻击者就能攻击(应该是 Padding Oracle 攻击)
3、使用对称加密的时候破解的对称加密

以上内容参考自知乎的博客中间人攻击

20、到底使用什么时候使用 GET 请求,什么时候使用 POST 请求?

点击看答案

当请求无副作用(如搜索)的时候,可以使用 GET 方法;当请求有副作用(如添加数据)的时候,则使用 POST 。不过有个很实际的问题: GET 方法可能会产生很长的 url ,并且某些浏览器和服务器对 GET 请求的 url 长度是有限制的 !

若符合下列任一情况,用 POST :

  • 请求的结果有持续性的副作用
  • 使用 GET 方法会使 url 过长
  • 要传送的数据不是采用 7 位的 ASCII 编码

若符合下列任一情况,用 GET:

  • 请求是为了查找资源
  • 请求的结果无持续性副作用
  • 收集的数据及 HTML 表单内输入字段名称的总长度不超过 1024 个字符

以上内容参考自 cnblogs 博客

21、TCP相关问题

粘包处理

TCP 两端 A、B 建立了连接后,A 端先发送 100 个字节,再发送 100 个字节。那么 B 端会分别收到两次 100 字节吗? 答案是不一定

我们常说 TCP 是一种流式连接,这个流字到底怎么理解?它是指 TCP 的数据传输就像一种水流一样,并不区分不同数据包之间的界限。

看过 TCP 协议内容的人就可以发现,TCP 协议允许发送端将几次发送的数据包缓存起来合成一个数据包发送到网络上去,因为这样可以获得更高的效率,这一行为通常是在操作系统提供的 SOCKET 中实现,所以在应用层对此毫无所觉。 所以我们在程序中调用 send 发送超过 MTU 的数据包时,操作系统提供的 SOCKET 的 send 发送了数据后,操作系统有可能缓存了起来,等待后续的数据一起发送,而不是立即发送出去。

分包发送 网络传输的概念中有 MTU 的概念,也即是网络中一个数据包最大的长度。如果要发送超过这个长度的数据包,就需要分包发送。当调用 SOCKET 的 send 发送超过 MTU 的数据包时,操作系统提供的 SOCKET 实现会自动将这个数据包分割成几个不超过 MTU 的数据包发送。 当出现这些上面这些情况的时候,接收端就会发现接收到的数据和发送的数据的次数不一致。这个就是粘包现象

问题

当我们传输如文件这种数据时,流式的传输非常适合,但是当我们传输指令之类的数据结构时,流式模型就有一个问题:无法知道指令的结束。所以粘包问题是必须要解决的

粘包问题解决方案

  • 短连接。需要发送数据的时候建立 TCP 连接,发送完一个数据包后就断开 TCP 连接,这样接收端自然就知道数据结束了。这种方案多次建立 TCP 连接,性能低下
  • 长连接。使用长连接能够获得更好的性能但不可避免的会遇到如何判断数据结构的开始与结束的问题。
  • 定长结构。如果发送端发送了一个固定长度的数据结构,接收端必须每次都严格判断接收到额数据的长度,当收到的数据长度不足时,需要再次接收数据,直到满足长度,当收到的数据多于固定长度时,需要截断数据,并将多余的数据缓存起来,视为长度不足需要再次接收处理。 不定长结构
    定长的数据结构是一种理想的情况,真正的应用中通常使用的都是不定长的数据结构。
  • 不定长结构。目前最通用的做法是在每次发送的数据的固定偏移位置写入数据包的长度所以对于处理粘包的关键在于提前获取到数据包的长度

心跳

当 TCP 两端 A、B 建立了连接后,如果一端拔掉网线或者拔掉电源,那么另一端能够收到通知吗?

答案是不会

TCP 的建立连接和断开连接都是通过发送数据实现的,也就是我们常说的三次握手、四次挥手。

但是这就说明了一点,如果不发送数据那么是无法断开连接的。正常情况下当 TCP 的一端A调用了 SOCKET 的 close 或者进程结束,操作系统就会按照 TCP 协议发送FIN 数据报文。B 端收到后就会断开连接。但是当出现了上文所说的异常情况时:被拔掉网线或者断掉电源,总结起来就是没有机会发出断开的 FIN 数据报文。那么和 A 直连的路由设备虽然知道 A 设备已经断开了,但是路由设备并没有保存连接的状态信息,所以路由设备也就不可能去通知 B 端 A 端的断开。而 B 端没有收到断开的数据报文就会依然保持连接。所以 A 端拔掉网线或者断掉电源后 B 端是没办法收到断开连接的通知的。

解决方案

KEEP_ALIVE

TCP 协议本身就提供了一种这样的机制来探测对端的存活。TCP 协议有一个KEEP_LIVE 开关,只要打开这个开关就会定时发送一些数据长度为零的探测心跳包,发送的频率和次数都可以设置

数据校验

TCP 如何保证数据的正确性,保证数据内容不会出错?

大部分人就会开始说丢包重传、接收确认之类的东西,但这些都扯偏了,只要少数人能够正确回答题目要问的问题:首部校验

对于能答上这个问题的人,我会进一步问,这个校验机制能够确保数据传输不会出错吗?

答案是不能

TCP 协议中规定,TCP 的首部字段中有一个字段是校验和,发送方将伪首部、TCP 首部、TCP 数据使用累加和校验的方式计算出一个数字,然后存放在首部的校验和字段里,接收者收到 TCP 包后重复这个过程,然后将计算出的校验和和接收到的首部中的校验和比较,如果不一致则说明数据在传输过程中出错。这就是TCP 的数据校验机制。

但是这个机制能够保证检查出一切错误吗?显然不能。因为这种校验方式是累加和,也就是将一系列的数字(TCP 协议规定的是数据中的每 16 个比特位数据作为一个数字)求和后取末位。

但是小学生都知道 A+B=B+A。

解决方案

在应用层添加一层校验

以上内容参考自csdn的博客

1、okhttp的理解

点击看答案

首先看下okhttp 的整个工作流程:

okhttp工作流程

  1. 首先通过 Builder 创建 OkhttpClient 对象
  2. 根据设定的条件,使用Request.Builder 构建request 对象
  3. 根据 OkhttpClient 与 request 创建 RealCall
  4. 不论是通过execute 还是enqueue 方式异步执行,最终都通过 getResponseWithInterceptorChain 方式获取Http 的返回结果
  5. 步骤4中,通过Okio 封装的Socket 请求网络,并获取返回结果

Interceptor 是Okhttp 中最重要的一个东西,它不止拦截请求执行一些额外处理,实际上实际的网络请求、缓存、透明压缩等都是通过Interceptor 实现,它们一起连成 Interceptor.chain,每一个Interceptor 决定它自己能处理哪些事件,如果不能处理,则交给下一个Interceptor 处理,也就是责任链模式。这很类似View 中对点击事件的处理。

以上内容参考自:okhttp 解析

okhttp有几种发起请求的方式?

两种,分别是:

  • 同步请求,将同步请求任务加入调度器的同步执行的双端队列(即runningSyncCalls,意为正在执行) ,然后直接调用 getResponseWithInterceptorChain 返回结果
  • 异步请求,异步请求加入调度器,经历 readyAsyncCalls 和 runningAsyncCalls,之后调用 getResponseWithInterceptorChain

同主机任务最多支持5个并发,同时执行的任务不超过64个。注意,任务执行完成之后,不管同步还是异步,都会将任务从队列中清除

okhttp的interceptor怎么实现责任链?

  • 实现 Interceptor接口,重写intercept方法
  • 做自己需要的处理,比如更改header之后需要重新builder出request,如果不涉及这块,可以不用重新build
  • 使用chain.proceed(request)将请求传到给下一级,并且会有response返回
  • 可以对response进行处理,比如根据response重试之类的,如果不做处理就将response作为结果抛给上一级处理

okhttp的调度器

Dispatcher维护了三个队列,分别是: 同步正在执行队列、异步准备执行队列 以及 异步正在执行队列

有哪些拦截器

  • CacheInterceptor:处理cache相关处理,如果本地有了可⽤的Cache,就可以在没有网络交互的情况下就返回缓存结果
  • addInterceptor(Interceptor),就是我们自定义的一些拦截器,在所有的拦截器处理之前进行最早的拦截处理,比如一些公共参数,Header都可以在这里添加
  • ConnectInterceptor,这里主要就是负责建立连接了,会建立TCP连接或者TLS连接
  • networkInterceptors,这里也是开发者自己设置的,但是由于位置不同,所以用处也不同。这个位置添加的拦截器可以看到请求和响应的数据了,所以可以做一些网络调试。它对应 addNetworkInterceptor 方法
  • RetryAndFollowUpInterceptor,这里会对连接做一些初始化工作,以及请求失败的充实工作,重定向的后续请求工作
  • 。。。未完待续,下次做笔记写完整

okhttp的线程池怎么实现

查看Dispatcher类这个代码就可以知道:

1
2
3
4
5
6
7
8
9
@get:Synchronized
@get:JvmName("executorService") val executorService: ExecutorService
get() {
if (executorServiceOrNull == null) {
executorServiceOrNull = ThreadPoolExecutor(0, Int.MAX_VALUE, 60, TimeUnit.SECONDS,
SynchronousQueue(), threadFactory("$okHttpName Dispatcher", false))
}
return executorServiceOrNull!!
}

将线程池的核心线程数设置为0;线程池容纳的最大线程数量为 Int.MAX_VALUE;超时时间设置为 60s ;队列设置为同步队列 SynchronousQueue ,先来先服务;

OkHttp线程池设计为核心线程为0是因为客户端可能在一段时间内不会有网络请求,为了避免浪费不必要的线程内存,所以不保留最低线程,同时最大线程设置为Int.MAX_VALUE为了防止同一时间有大量的请求进入,造成部分请求被抛弃的问题,设置60秒为线程空闲最大时间,在一段时间不使用的情况进行线程回收。

SynchronousQueue每个插入操作必须等待另一个线程的移除操作,同样任何一个移除操作都等待另一个线程的插入操作。因此队列内部其实没有任何一个元素,或者说容量为0。因此我们可以理解来了任务直接执行

还有要注意的一点就是,如果异步请求中,runningAsyncCalls 的数量大于64后,就会加入到 readyAsyncCall 排队等待

okhttp用到什么设计模式

  • 责任链模式(拦截器)
  • 建造者(比如 Request 就是通过建造者模式建造出来的)
  • 工厂模式(比如CacheInterCeptor中的策略工厂 CacheStrategy.Factory)
  • 单例模式

以上内容参考csdn简书简书2

2、谈谈对RxJava的理解

点击看答案

常用操作符

  • map:将被观察者发送的数据类型转换为其他类型
  • flatMap : 将事件序列中的元素整合,返回一个新的被观察者
  • zip : 将多个观察者事件整合发送给观察者

如何实现线程切换

  • Observer 最终会封装成 SubscribeTask ,这个类实现了 Runnable 接口。
  • 最终在 Runnable 的run 方法中完成 观察者-被观察者的订阅关系
  • 也即,这个run 在哪个线程执行,observer 方法就在哪个方法执行。
  • 如果是 AndroidSchedulers.mainThread 的话,则会以Android主线程的Looper(Looper.getMainLooper())新建一个 Handler ,之后将上述Runnable 封装成Message ,通过Handler 发送到主线程。
  • 如果是其他线程,则会丢给特定线程或者线程池处理。

以上内容参考自: rxjava2线程切换原理rxjava使用与原理

3、fresco的理解

点击看答案

为什么使用fresco

  1. 部门决定采用webp 格式的图片,而fresco 对其支持
  2. 5.0 以下Android系统,使用 ashmem(匿名共享内存) 区域存储bitmap ,它的创建、释放都不会触发 GC,带来良好的性能。

fresco 使用ashmem 区域存储bitmap ,gc不会处理这块区域,并且也不会被”ashmem内置清除机制”回收,所以减少gc,提升性能。在ashmem 中,fresco 采用引用计数方式,自己管理内存。

  1. 使用了三级缓存,方便图片快速复用、加载:Bitmap 缓存 + 未解码缓存 + 硬盘缓存,前面两个是内存缓存,Bitmap 根据不同系统版本存放不同区域(5.0以下存放ashmem),未解码存放在堆内存。
  2. fresco 的设计,UIThread 只做从内存缓存中加载图片和显示图片两件事,其他诸如 图片Decode、缓存读写 都放在非 UI线程。

fresco 原理解析

典型的MVC模式应用:

  • DraweeView : view 层,负责显示图片。它继承ImageView 的目的是使用它来显示 drawable ,其他的ImageView 方法都没有使用,也不推荐使用。
  • Hierarchy: model 层,负责生成要显示的图片
  • DraweeController: controller 层

DraweeView 把获得event 转给 controller,controller 决定是否隐藏或者显示什么图像,而这些图像存储在 Hierarchy,最后 DraweeView 直接通过 getTopLevelDrawable 获取要显示的图像。

DraweeView 不直接与 Hierarchy 及 DraweeController 打交道,而是通过 DraweeHolder 间接与他们打交道,因为 DraweeHolder 管理着 Hierarchy 与 Controller。

图片库的选择

Picasso

自己没有实现缓存,配合 Okhttp 在 Okhttp里面实现缓存

优点:

  • 与Square系的库搭配较好,如okhttp、retrofit等
  • 包小
  • 功能简单

Glide

优点:

  • 支持webp、gif、video
  • 支持Memory和Disk缓存
  • 默认RGB_565,开销小

fresco

优点

  • 支持webp图片

  • native层缓存图片,减少oom

  • 使用简单,几乎都能在xml上搞定

缺点:

  • 太大

fresco 图片切换原理

DraweeHierarchy 内部维护着一个Drawable序列,这些个Drawable代表不同层次的图片,如果没有设置,这层Drawable就会为null,如果设置了但此时不应该展示它,比如 ActualImage 已经加载到了,不需要placeHold图片了,就把这层Drawable的 alpha 置为 0 。

准确地说是 FadeDrawable 中包含了上述的Drawable,在调用 FadeDrawable 的onDraw() 方法时,就会一层一层绘制,如果该层为null或者alpha为0,就不绘制,这样就实现了切换。

关于Ashmem

Ashmem不能被Java应用直接处理,但是也有一些例外,图片就是其中之一.当你创建一张没有经过压缩的Bitmap的时候,Android的API允许你指定是否是可清除的:

1
2
3
BitmapFactory.Options = new BitmapFactory.Options();
options.inPurgeable = true;
Bitmap bitmap = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length, options);

经过以上处理,当 Android 绘制系统在渲染这些图片,Android 的系统库就会把这些 Bitmap 从 Ashmem 堆中抽取出来,而当渲染结束后,这些 Bitmap 又会被放回到原来的位置。如果一个被抽取的图片需要再绘制一次,系统仅仅需要把它再解码一次,这个操作非常迅速。

Fresco 如何将图片放到Ashmem中?Ashmem一般在应用层是无法直接访问的,除了几个特例之外,其中之一就是 decode bitmap。我们可以通过设置BitmapFactory.Options.inPurgeable = true 来创建一个Purgeable Bitmap,这样decode出来的bitmap是在Ashmem内存中的,GC无法直接回收它。当Bitmap在使用时会被pin住,这样就不会被释放,使用完之后就unpin,这样系统在未来某个时刻会释放这部分内存。如果unpin的图片后续又要使用,就得再次decode,如果是在ui线程执行decode,就可能掉帧,因此google建议使用 inBitmap 来尝试使用已经存在的内存区域,而不是新分配区域,不过,要使用inBitmap ,必须要求二者有相同的解码格式,比如都是8888或者都是 565 的。

Fresco 为了让inPurgeable的bitmap不被自动unpinned,可以使用jni函数 AndroidBitmap_lockPixels()来强制pin bitmap,这样避免在unpinned之后,重新decode 这个Bitmap 而引起掉帧,不过这样就需要自己来管理这块空间了,我们可以使用 AndroidBitmap_unlockPixels 来让bitmap 重新变为 unpinned 状态。这样,系统在内存不足的时候,就可以回收这块内存了。Fresco使用Ashmem这块的知识点详细参考这篇文章

参考itpubjcodeer简书

4、ThreadLocal 详解

点击看答案

ThreadLocal 很典型的一个用处就是存储线程的 Looper,我们知道,子线程中初始化Handler 的时候,需要先执行 Looper.prepare ,这个操作就是新建一个Looper 并且将其保存到 ThreadLocal 中。

Thread 类中有个专门存储线程的 ThreadLocal 数据的结构,即 ThreadLocal.Values 。保存值时,首先通过 Thread.currentThread 获取到当前线程,再获取该线程的 ThreadLocal.Values ,这个 Values 中有个 Object[] table 的数组,ThreadLocal 对象就存在这个数组中。每个 ThreadLocal 对象根据自己的 hashcode 按照一定规则获取到在数组中的 index ,之后进行读取或者存储。

这样,每个线程通过同一个 ThreadLocal 获取到的是不同的值。各个线程可以相互独立地执行操作。

以上内容总结自源码,部分语言参考自任玉刚的博客内容

5、LocalBroadcastManager

点击看答案

LocalBroadcastManager 注册本地广播只能通过代码,不能通过xml静态注册。本地广播不会跨进程,不用跟system_server 交互。

原理分析

首先,LocalBroadcastManager.getInstance 是个单例,在初始化过程中,会根据 mainLooper 创建一个Handler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private LocalBroadcastManager(Context context) {
mAppContext = context;
mHandler = new Handler(context.getMainLooper()) {

@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_EXEC_PENDING_BROADCASTS:
executePendingBroadcasts();
break;
default:
super.handleMessage(msg);
}
}
};
}

其次,注册过程,其实可以理解成订阅某种消息,以便在符合条件的消息发送的时候,这里能接收:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void registerReceiver(@NonNull BroadcastReceiver receiver, @NonNull IntentFilter filter) {
synchronized (mReceivers) {
//创建ReceiverRecord对象
ReceiverRecord entry = new ReceiverRecord(filter, receiver);
//mReceivers:数据类型为HashMap<BroadcastReceiver, ArrayList>, 记录广播接收者与IntentFilter列表的对应关系
ArrayList<ReceiverRecord> filters = mReceivers.get(receiver);
if (filters == null) {
filters = new ArrayList<>(1);
mReceivers.put(receiver, filters);
}

filters.add(entry);
for (int i=0; i<filter.countActions(); i++) {
String action = filter.getAction(i);
//mActions:数据类型为HashMap<String, ArrayList>, 记录action与广播接收者的对应关系
ArrayList<ReceiverRecord> entries = mActions.get(action);
if (entries == null) {
entries = new ArrayList<ReceiverRecord>(1);
mActions.put(action, entries);
}
entries.add(entry);
}
}
}

接着,发送广播,可以理解为,根据 sendBroadcast(Intent intent) 中 intent 的值获取 actions,再根据action 来查询相应的广播接收者,当然,如果当前receiver 正在处理其他广播,则跳过:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
    public boolean sendBroadcast(Intent intent) {
synchronized (mReceivers) {
...

//根据Intent的action来查询相应的广播接收者列表
ArrayList<ReceiverRecord> entries = mActions.get(intent.getAction());
if (entries != null) {
//用于存放与当前action匹配的receiver
ArrayList<ReceiverRecord> receivers = null;
for (int i=0; i<entries.size(); i++) {
ReceiverRecord receiver = entries.get(i);
if (receiver.broadcasting) {
//当前receiver正在处理其他广播,则跳过
continue;
}

int match = receiver.filter.match(action, type, scheme, data,categories, "LocalBroadcastManager");
if (match >= 0) {
if (receivers == null) {
receivers = new ArrayList<ReceiverRecord>();
}
receivers.add(receiver);
receiver.broadcasting = true;
}
}

if (receivers != null) {
for (int i=0; i<receivers.size(); i++) {
receivers.get(i).broadcasting = false;
}
//创建相应广播,添加到mPendingBroadcasts队列
mPendingBroadcasts.add(new BroadcastRecord(intent, receivers));
if (!mHandler.hasMessages(MSG_EXEC_PENDING_BROADCASTS)) {
//发送消息【见小节2.3.1】
mHandler.sendEmptyMessage(MSG_EXEC_PENDING_BROADCASTS);
}
return true;
}
}
}
return false;
}

在 LocalBroadcastManager 的构造函数中我们初始化了这个以 mainLooper 建立的Handler,此时利用它 sendEmptyMessage,在handleMessage 中最终会调用 executePendingBroadcasts 方法(说明此函数也运行在主线程):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private void executePendingBroadcasts() {
while (true) {
final BroadcastRecord[] brs;
//将mPendingBroadcasts保存到brs数组
synchronized (mReceivers) {
final int N = mPendingBroadcasts.size();
if (N <= 0) {
return;
}
brs = new BroadcastRecord[N];
mPendingBroadcasts.toArray(brs);
mPendingBroadcasts.clear();
}
//挨个回调相应广播接收者的onReceive
for (int i=0; i<brs.length; i++) {
final BroadcastRecord br = brs[i];
final int nbr = br.receivers.size();
for (int j=0; j<nbr; j++) {
final ReceiverRecord rec = br.receivers.get(j);
if (!rec.dead) {
rec.receiver.onReceive(mAppContext, br.intent);
}
}
}
}
}

通过以上的原理分析,我们知道本地广播只是在发送广播的时候,在主线程中挨个通知 action 符合的receiver,因此并不会超出进程范围,也不会超出 app 范围,只会在当前app 的当前进程发生。

以上内容参考自gityuan的分析

6、Java线程池ThreadPoolExecutor实现原理

点击看答案

ThreadPoolExecutor 构造函数参数非常多,有以下:

  • corePoolSize: 通过 submit 或者 execute 提交任务时,如果当前线程池的线程数 n < corePoolSize ,则创建一个新的线程处理任务,即使其他 core 线程是空闲的。

  • maximumPoolSize: 如果当前线程数 n > corePoolSize && n < maximumPoolSize ,那么不会创建新的线程;但是如果 n >= maximumPoolSize 时,就会创建新的线程。如果是个无界队列(LinkedBlockingQueue),那么不存在满的情况(n >= maximumPoolSize),也就不会创建新线程。

  • keepAliveTime: 如果当前线程池中的线程数 n > corePoolSize,那么如果在 keepAliveTime 时间内没有新的任务需要处理,那么就会销毁 corePoolSize - n 个线程。

  • handler :异常处理策略。即当任务提交失败的时候,调用这个处理器。

运行状态

ThreadPoolExecutor 使用一个 AtomicInteger 的前三位表示线程池状态,后 29 位表示线程数,因此是可以支持上亿的线程计数。线程池主要有几种状态:

  • Running: 线程池正在运行,可以接收新任务。
  • ShutDown: 不再接收新任务,但会继续处理队列中任务。
  • Stop: 不接受新任务,也不处理队列中任务,并且中断正在处理的任务
  • Tidying: 所有任务处理玩,线程数为 0(线程池为空)
  • Terminated: 已经执行完毕(执行了 terminated)

submit 执行过程 就是将 Runnable 和 Callable 封装成 RunnableFuture 之后,最终提交给execute 执行。使用 HashSet 类型的 worker 来存储正在运行的任务,只要 worker.size() < corePoolSize,提交新的任务就马上开启新线程执行(上面提到过)。在提交过程中要检查线程池的状态,检查是否关闭了。

worker的数目也是通过 CAS的方式 增减的。

以上内容参考自github上的博客

7、延伸-Java 线程池的异常处理机制

点击看答案
  • 如果是使用submit 提交的话,可以通过继承 ThreadPoolExecutor 再重写 afterExecute 方法,得到实际的异常 (包含 Runnable 和 Throwable)
  • 如果是调用的execute 方法提交的话,那就会抛到 dispatchUncaughtException 里面去了,这时候我们只有对线程 Thread.setUncaughtExceptionHandler(UncaughtExceptionHandler) 来捕捉。即自己写 ThreadFactory (thread 工厂类),并为创建的线程 setUncaughtExceptionHandler
  • 还有一种,就是对 Runnable 的 run 方法里面整个 try-catch

以上内容参考自并发编程网 或者它在github上的相同文章

8、AsyncTask 解析

点击看答案

AsyncTask 是个抽象类,必须子类实现才能使用。在构建的时候,需要指定三个泛型参数类型,分别是 Params、Progress、Result ,即类似 AsyncTask<Integers, Integers, ResponseBean> ,当然,如果某个参数不需要,类型可以写成 Void 。

其整体原理还是 将task丢给ThreadPool 在子线程执行,得到结果后,通过 Handler 的 sendEmptyMessage 的方式将结果切换到主线程

在 AsyncTask 使用的过程中需要遵守如下原则:

  • 必须在UI线程中实例化
  • execute 必须在UI线程中调用
  • 不要人为调用 onPreExecute、onPostExecute、doInBackground 和 onProgressUpdate
  • 一个 AsyncTask 实例只能执行一次,如果多次调用会报异常

AsyncTask 中有 static 的 ThreadPool ,意味着不管有多少个实例,都只有这个线程池,而在初始化这个线程池的时候,corePoolSize 在不同版本的值默认被设置为 1 或者 5 (Android 3.0以前是5,还不能改;3.0之后设置为1,但是可以自己设置Excutor ),并且 BlockingQueue 基本上是个无界队列(BlockingQueue 或 SynchronousQueue,队列不存在满的情况),根据 ThreadPool 的原理,我们每次最多只有一个线程或者 5 个线程在执行,意味着多的任务就要排队,并不能实时执行,并且在早期,我们不能设置自定义的 ThreadPoolExcutor,到后来才可以(貌似是Android 4.0以后)。

AsyncTask 存在的问题:

  1. AsyncTask 对象只能execute 一次,多次请求会导致多个对象创建
  2. 生命周期与Activity 的生命周期不一致,有可能导致内存泄露
  3. cancle 并不马上生效,因为它就是线程,在cancle之后,还得等它完成

以上内容参考自 系统源码、github上的博客cnblogs的博客csdn的博客

9、阿里Alpha原理

点击看答案

想象下有以下场景:

有6个任务需要在Application里面执行,其中Task1,Task4,Tas6需要在主线程执行,Task2,Task3需要在Task1执行完才能执行,Task4,Task5需要Task2和Task3执行完才能执行,Task6需要Task4和Task5执行完才能执行,Task4的耗时要大于Task5,是不是顿时就乱了?其实可以通过 PERT 图来捋一捋这个关系,涉及到具体实现的话,可以参考阿里巴巴的 alpha 框架。

Alpha是一个基于PERT图构建的Android异步启动框架

首先解决多进程疑惑,在start方法中就首先判断了 主进程任务、非主进程任务 以及 适用于所有进程的任务,这些任务是通过 public void addProject(Task project, int mode) 方法添加进去的。

在实际情况中,可能会有多个任务同时开始,并且也有可能多个任务作为结束节点,所以为了方便控制整个流程,alpha 设计了startTask 和 finishTask,标记流程的开始和结束,方便任务的监听

如果Task 是在主线程执行的,那么就通过Handler 将时间传递给主线程;如果是非主线程,则通过线程池去执行。

在一个Task执行完成后,就会遍历自己持有的 mSuccessorList(紧后任务列表,也就是当前任务执行完成之后可以执行的Task列表,这里面的Task会根据Priority进行排序),并依次执行里面元素的 onPredecessorFinished 方法。

mSuccessorList 列表中的Task 是通过 after 方法加入的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//紧后任务添加
public Builder after(Task task) {
task.addSuccessor(mCacheTask);
mFinishTask.removePredecessor(task);
mIsSetPosition = true;
return Builder.this;
}

//主要操作在这个方法里
void addSuccessor(Task task) {
//task 添加紧前任务
task.addPredecessor(this);
//真正添加为紧后任务
mSuccessorList.add(task);
}

意思是Task2要在Task1后面执行,这样,Task2就是Task1的紧后任务,同理,Task1也成了Task2的紧前任务,那这个紧前任务有什么用呢?试想一下,如果Task1、Task2、Task3的紧后任务都是 Task4,那么,在Task1执行完成之后,还要判断 Task2和Task3是否执行完成,然后才能决定是否执行Task4,这就是紧前任务的作用了。

以上文章主要参考自:积木zz的csdn博客, 有博客说,使用 Anchors 比使用 Alpha 更精细,后续再看

10、LeakCanary 原理

点击看答案

原理:

  1. 通过registerActivityLifecycleCallbacks 来监听 Activity 的生命周期 onActivityDestroyed。
  2. 即 lifecycleCallbacks 监听Activity 的 onDestroy 方法,正常情况下执行了onDestroy 后资源立即回收。
  3. 监察机制利用了 WeakReference 和 ReferenceQueue ,使用 WeakReference 对Activity 进行引用,在Activity回收的时候,就会将该WeakReference 引用放到 ReferenceQueue 中。
  4. 在onDestroy 之后,等待一段时间,再通过监测 ReferenceQueue 是否包含 WeakReference 就能检查 Activity 是否被正确回收。
  5. 如果Activity 没有被回收,就手动 GC 一次,等待若干时间,之后再次判断Activity 是否被回收,若未被回收,说明 Activity 已经泄露。
  6. 如果Activity 泄露了,则抓取 dump 信息显示出来。

以上要注意的是:

1、是使用WeakReference对Activity进行引用
2、LeakCanary可以配置忽略某些路径的内存泄漏
3、手动GC是使用的 Runtime.getRuntime().gc() 实现,代码中解释是这样触发gc的概率会比System.gc() 高一些: System.gc() does not garbage collect every time. Runtime.gc() is more likely to perform a gc.
4、 当Activity对象被回收时,会将 WeakReference(而不是Activity)对象放入 ReferenceQueue 中,自己写的测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class MainActivity extends AppCompatActivity {
private RecyclebleObject testObject;
private WeakReference<RecyclebleObject> objectWeakReference;
private ReferenceQueue<RecyclebleObject> referenceQueue;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

testObject = new RecyclebleObject();
referenceQueue = new ReferenceQueue<>();
objectWeakReference = new WeakReference<>(testObject, referenceQueue);

final TextView btnClick = findViewById(R.id.click_view);

btnClick.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (testObject == null) {
testObject = new RecyclebleObject();
}

Log.e("NullTest", "testObject = " + testObject);
Log.e("NullTest", "referenceQueue 中的内容: " + referenceQueue.poll());
testObject = null;
Runtime.getRuntime().gc();

btnClick.postDelayed(new Runnable() {
@Override
public void run() {
Log.e("NullTest", "GC 后,testObject = " + testObject);
Reference result = referenceQueue.poll();
Log.e("NullTest", "GC 后,referenceQueue 的内容: " + result + ",这个对象与objectWeakReference 相等吗? " + (result == objectWeakReference));
}
}, 1000);
}
});
}
}

以上代码将会打印出以下结果:

testObject = com.example.myapplication.RecyclebleObject@86f4db5
referenceQueue 中的内容: null
GC 后,testObject = null
GC 后,referenceQueue 的内容: java.lang.ref.WeakReference@f6c4a,这个对象与objectWeakReference 相等吗? true

说明在回收后, WeakReference 对象会出现在 referenceQueue 中,而不是 testObject 本身出现在 referenceQueue 。


有意思的是,在 LeakCanary2 时,并不需要接入者手动初始化(LeakCanary.install(this);)了,而是只需要引入即可。其根本原理是:LeakCanary 写了个 ContentProvider 并在 AndroidManifest中注册了,并在 ContentProvider 的onCreate方法中执行了 install 操作!我们知道,ContentProvider 的 onCreate 方法会在启动App的时候自动执行,并且比 Application 的 onCreate 方法还要早,因此它自动执行完全没问题。

以上内容参考自JasmineBen的博客CSDN上的博客、以及自己写的代码验证

11、Toast显示流程

点击看答案

首先,为了避免Toast显示冲突,会将要显示的Toast放在队列中,然后依次取出来展示

  1. makeText的时候,创建Toast对象和TN对象,Toast创建好后,加载布局,创建mNextView,然后 TN 是控制Toast的显示和隐藏以及取消的,它里面有个 Handler ,以当前线程的Looper来初始化,Toast的显示隐藏取消就是通过这个Handler来处理的。
  2. Toast对象创建完成就 enqueueToast 到NotificationManagerService 进程中去排队的(所以这中间是有跨进程通信这个概念的),在中间会判断是否要显示这个Toast(如果此Toast正在取消或者隐藏就不展示了),接着就开始排队,显示的话,就是不断从队列里面取出 ToastRecord ,然后调用 Toast对应的TN 的show 方法展示Toast。
  3. TN收到显示的消息,创建WindowManager对象,然后将第一步创建的 View 添加到 WindowManager ,之后Toast 就显示出来了。

Toast显示流程

以上内容参考自简书上的博客

12、onTouchListener、onTouchListener 的onTouch方法、onTouchEvent、onClickListener、onLongClickListener 的执行顺序?

点击看答案
  • dispatchTouchEvent 在 onTouchListener 之前发生,如果在 dispatchTouchEvent 的 down 事件就return 了 false,则后续的事件压根就不会传过来了,所以不会有什么故事。
  • 但如果 down 事件返回了 true ,则事件虽然onTouchListener 和 onTouchEvent 会发生,但不会有点击事件了,即onLongClick 和 onClick 都不会响应了。
  • onTouchListener 在onTouchEvent 之前发生, onLongClick 与 onClick 依赖于是在onTouchEvent 里面发生的,也就是说 click 事件是在 onTouchEvent 之后执行,并且 onClick 在 onLongClick 之后执行。
  • 如果在 onTouchListener 的 onTouch 方法中返回true,则没有后面的 onTouchEvent 什么事了,更别提 click 事件
  • 在onTouch 发生后,如果直接在 onTouchEvent 中返回true 或者false ,那就没有 click 什么事情了(因为click 是在super.onTouchEvent中)
  • 如果TouchEvent 不做处理,那么在down事件发生后长按,则会响应 onLongClick 事件,之后up,如果之前的 onLongClick 返回false ,则还会接着 onClick,反之,如果之前的 onLongClick 返回true,则 onClick 不会执行。

以上内容来自自己的实验,以及csdn上的博客1csdn上的博客2

13、Android 事件中 CANCLE 事件是怎么来的?它的作用是啥?

点击看答案

第一个问题:我们知道view如果处理了 Down 事件,则随之而来的 Move 和 Up 事件也会交给它处理,但是交给它处理之前,父View 可以拦截,如果被拦截了,就会返回 Cancel 事件,并且不会收到后续的 Move 和 Up 事件

第二个问题:

以上内容参考自 csdn上的博客

14、Handler 解析

点击看答案

构造函数

Handler 有多个构造函数,看下图:

Handler构造函数

这些参数有几个需要解释下,callback:用于控制消息执行顺序的,具体参看Handler 的 dispatchMessage 方法。

1
2
3
4
5
6
7
8
9
10
11
12
public void dispatchMessage(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}

在执行时,如果 msg 设置了 callback,则优先执行,接下来,如果Handler 有设置了 callback ,则执行这个callback ,最后才是我们熟悉的 handleMessage 方法。因此,我们说 这个 callback 是控制执行顺序的。

Looper 参数表明的是,Handler 将在哪个线程执行,使用哪个线程的。

boolean 类型的 async 值是指 Handler 是否发送异步消息,这个异步消息要配合消息屏障使用。首先设置了消息屏障,之后 Looper 中只会执行异步消息了,直到消息屏障被 remove 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Message next() {

...

for (;;) {
synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());

...

}
}
}
}

15、ViewStub源码解析

点击看答案

首先,在构造函数里面有两个点值得关注:

1
2
3
4
5
6
7
public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
...
//置为不可见
setVisibility(GONE);
//置为不会draw
setWillNotDraw(true);
}

构造方法里面就直接将ViewStub置为不可见,并且,设置为不会 draw ,因为ViewStub本身不展示,所以无需 draw 。然后,我们一般都是通过inflate 或者 setVisibility 来设置ViewStub的可见性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public void setVisibility(int visibility) {
if (mInflatedViewRef != null) {
View view = mInflatedViewRef.get();
if (view != null) {
view.setVisibility(visibility);
} else {
throw new IllegalStateException("setVisibility called on un-referenced view");
}
} else {
super.setVisibility(visibility);
if (visibility == VISIBLE || visibility == INVISIBLE) {
inflate();
}
}
}


public View inflate() {
//获取VIewStub的parent
final ViewParent viewParent = getParent();

if (viewParent != null && viewParent instanceof ViewGroup) {
if (mLayoutResource != 0) {
final ViewGroup parent = (ViewGroup) viewParent;
//也就是 .inflate(mLayoutResource, parent, false) 最后一个参数为false
final View view = inflateViewNoAdd(parent);
replaceSelfWithView(view, parent);

mInflatedViewRef = new WeakReference<>(view);
if (mInflateListener != null) {
mInflateListener.onInflate(this, view);
}

return view;
} else {
throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
}
} else {
throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
}
}

由以上代码可以看到,调用 setVisibility 时,如果以前有inflate出来真正的 view 了,那就直接对这个 view.setVisibility ;否则,如果 setVisibility 为 visible 或者 invisible ,都会触发 inflate 操作。

inflate 操作,如果没有指定真正的 view 的布局id (mLayoutResource),那会抛出异常,之后,将真正的 view 以 ViewStub的parent 作为 parent 先inflate 出来,接下来 replaceSelfWithView 其实就是将真正的 view 替换到原来 viewstub的位置(位置 index 和布局参数 layoutparams 都拿过去),而 原来的 ViewStub 会被remove 掉:

1
2
3
4
5
6
7
8
9
10
11
private void replaceSelfWithView(View view, ViewGroup parent) {
final int index = parent.indexOfChild(this);
parent.removeViewInLayout(this);

final ViewGroup.LayoutParams layoutParams = getLayoutParams();
if (layoutParams != null) {
parent.addView(view, index, layoutParams);
} else {
parent.addView(view, index);
}
}

为何无大小不绘制

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(0, 0);
}

@Override
public void draw(Canvas canvas) {
}

@Override
protected void dispatchDraw(Canvas canvas) {
}

从源码可以直接看到,在 onMeasure 中,直接 setMeasuredDimension(0, 0) 即宽和高都变为 0 了,并且draw 和 dispatchDraw 都是空方法。

能inflate多次吗

还是看inflate 的源码:

1
2
3
4
5
6
7
8
9
public View inflate() {
final ViewParent viewParent = getParent();

if (viewParent != null && viewParent instanceof ViewGroup) {
...
} else {
throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
}
}

能够看到,inflate 的时候,这里会获取 ViewStub 自己的 parent,然后呢,会判断 viewParent != null,由于前面说了,inflate 的时候已经将 ViewStub 从patent 中移除,所以这里肯定为 null ,因此,这就会报错啦。所以我们只能inflate一次

以上内容参考AS打开的源码,以及CSDN上的观点

16、事件传递顺序

点击看答案

首先,由源代码可知:

1
2
3
4
5
6
7
public boolean dispatchTouchEvent(MotionEvent event) {  
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}

首先执行 dispatchTouchEvent ,其次 执行 OnTouchListener.onTouch,如果返回true ,则不会执行后续的 onTouchEvent 了

其次,看 onTouchEvent 的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public boolean onTouchEvent(MotionEvent event) {  
...
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
...
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent){
// This is a tap, so remove the longpress check
removeLongPressCallback();
...

if (mPerformClick == null) {
mPerformClick = new PerformClick();
}

//触发OnClick事件
if (!post(mPerformClick)) {
performClick();
}
}
break;
case MotionEvent.ACTION_DOWN:
if (isInScrollingContainer) {
...
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
// 触发事件
checkForLongClick(0, x, y);
}
break;
...
}
//如果该控件是可以点击的,就一定会返回true
return true;
}
//如果该控件是不可以点击的,就一定会返回false
return false;
}

由此可知,onLongClickListener 、onClickListerner 都是在 onTouchEvent 中触发的,前者是在DOWN事件中触发,后者是在UP事件中触发。如果 onLongClickListener 执行了,onClickListerner 就不会执行。

综上,dispatchTouchEvent -> onTouchListener -> onTouchEvent -> onLongClick -> onClick 的顺序

参考这这里

17、EventBus 原理

点击看答案

注: 本文的EventBus版本为 3.0

Subscrib 注解

自 3.0 以来,EventBus 使用 @Subscrib 注解来标记订阅事件的方法,方法命名随意。并不用像以前那样指定方法的命名:

1
2
@Subscribe
public void testEventBus(Object obj){ }

这个注解的定义很有个性,可以看下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Documented
@Retention(RetentionPolicy.RUNTIME) // 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在
@Target({ElementType.METHOD}) // 作用在方法上
public @interface Subscribe {

// 指定事件订阅方法所在的线程模式,也就是决定订阅方法是在哪个线程,默认是POSTING模式
ThreadMode threadMode() default ThreadMode.POSTING;

// 是否支持粘性事件
boolean sticky() default false;

// 优先级,如果指定了优先级,则若干方法接收同一事件时,优先级高的方法会先接收到。
int priority() default 0;
}

其中有几种线程模式,有以下几种:

  • ThreadMode.MAIN:如在主线程(UI线程)发送事件,则直接在主线程处理事件;如果在子线程发送事件,则先将事件入队列,然后通过 Handler 切换到主线程,依次处理事件。

  • ThreadMode.ASYNC:与ThreadMode.MAIN_ORDERED相反,无论在哪个线程发送事件,都将事件加入到队列中,然后通过线程池执行事件

  • ThreadMode.POSTING:默认的线程模式,在哪个线程发送事件就在对应线程处理事件,避免了线程切换,效率高。

  • ThreadMode.MAIN_ORDERED:无论在哪个线程发送事件,都将事件加入到队列中,然后通过Handler切换到主线程,依次处理事件。

  • ThreadMode.BACKGROUND:与ThreadMode.MAIN相反,如果在子线程发送事件,则直接在子线程处理事件;如果在主线程上发送事件,则先将事件入队列,然后通过线程池处理事件。

注册

注册过程很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
EventBus.getDefault().register(this);

//注册源码
public void register(Object subscriber) {
Class<?> subscriberClass = subscriber.getClass(); // 获取传入的要注册类的字节码文件
List<SubscriberMethod> subscriberMethods =
subscriberMethodFinder.findSubscriberMethods(subscriberClass);

synchronized (this) {

// 遍历订阅方法封装类的集合
for (SubscriberMethod subscriberMethod : subscriberMethods) {
subscribe(subscriber, subscriberMethod);
}
}
}

根据源码可以直到,这个方法就做了两件事情:

  • 根据传入的参数object ,获取其 Class ,然后通过这个 Class 获取所有的方法(当然,首先看有没有缓存),查看经过 @Subscrib 修饰的方法,即这个类中所有的订阅方法(会做封装),生成一个list
  • 遍历上述生成的 list ,给 2 个 Map 填充数据:subscriptionsByEventType以 event (订阅方法的参数)的类型(Class)为key,value 为订阅方法list(CopyOnWriteArrayList);typesBySubscriber 以register时传入的对象为key ,value 为 这个对象所有订阅方法所订阅的事件。
1
2
3
4
5
6
//EventBus中变量的声明

//可以根据event(订阅方法中的参数)类型 获取所有订阅方法
private final Map<Class<?>, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType;
//根据注册对象,获取这个对象上所有的订阅方法
private final Map<Object, List<Class<?>>> typesBySubscriber;

反注册

反注册也很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
EventBus.getDefault().unregister(this);

public synchronized void unregister(Object subscriber) {

List<Class<?>> subscribedTypes = typesBySubscriber.get(subscriber);

// 如果集合不为null
if (subscribedTypes != null) {

// 遍历集合,获取订阅事件的类型
for (Class<?> eventType : subscribedTypes) {

unsubscribeByEventType(subscriber, eventType);
}
typesBySubscriber.remove(subscriber);
} else {
logger.log(Level.WARNING, "Subscriber to unregister was not registered before: " + subscriber.getClass());
}
}

刚才在注册时候,说了其中一个 map 为 typesBySubscriber,它以注册对象为 key ,value 为这个对象中所有注册方法所注册的event 类型的列表。所以在反注册的时候,

  1. 首先通过传入的对象,获取 注册的 event 的列表
  2. 遍历这个列表,获取 event 的类型,然后通过这个类型在 subscriptionsByEventType 查找(经过封装的)订阅方法,根据封装在里面的 注册对象 是否是当前 unregister 传入的对象来判断,如果是当前传入的对象,就移除这个经过封装的订阅方法

post 发布事件

post的使用也很简单:

1
EventBus.getDefault().post(new Object());

注册的时候,说了有一个map 为 subscriptionsByEventType ,以 event 的类型为key ,存储了所有订阅了这中 event 的(经过封装的)方法。post 的时候,根据post发送的事件类型(post方法的参数的 Class )从 subscriptionsByEventType 这个集合中获取到所有的订阅方法。之后依次通过反射调用这些方法:

1
2
3
4
5
6
7
8
9
10
void invokeSubscriber(Subscription subscription, Object event) {

try {
subscription.subscriberMethod.method.invoke(subscription.subscriber, event);
} catch (InvocationTargetException e) {
handleSubscriberException(subscription, event, e.getCause());
} catch (IllegalAccessException e) {
throw new IllegalStateException("Unexpected exception", e);
}
}

其中, subscription.subscriberMethod.method 是 Method 类型的,用过反射的话,就知道它可以直接 invoke ,它的定义是这样的:

1
public native Object invoke(Object obj, Object... args)

第一个参数 obj 指的是用 obj 这个实例来调用这个方法(因为一个类可能会有多个实例,非静态方法需要指定一个实例来调用这个方法),后面的 args 就是方法需要传入的参数。

所以,在 invoke 的时候,subscription.subscriber 我们应该很容易知道是 register 时传入的那个 Object !由此,我们一个消息就形成了闭环。

最后问题,支持跨进程吗?

先说结论:不支持跨进程,因为单例。经过上述分析,我们知道 EventBus 的注册、反注册、post 都是通过:EventBus.getDefault() 实现,我们看下它的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class EventBus {

static volatile EventBus defaultInstance;

/** Convenience singleton for apps using a process-wide EventBus instance. */
public static EventBus getDefault() {
EventBus instance = defaultInstance;
if (instance == null) {
synchronized (EventBus.class) {
instance = EventBus.defaultInstance;
if (instance == null) {
instance = EventBus.defaultInstance = new EventBus();
}
}
}
return instance;
}

所以我们都是基于一个 static 类型的 defaultInstance去做一系列操作,由于跨进程后,静态会失效,所以,EventBus 并不能跨进程

以上参考自掘金刘洋巴金的博客,如果链接变得不可访问,可以参看这篇文章的Copy

18、ARouter 的原理

点击看答案

为了让业务逻辑彻底解耦,同时也为了每个module 都可以方便地单独运行和调试,上层的各个 module 不会相互依赖,而是共同依赖 base module,如下所示:

业务module依赖base_module

由于module之间没有依赖,那如何实现业务跳转呢? 首先,隐式跳转是一种可行的解决方案,但如果一个项目里面全是隐式跳转的,就会导致 Manifest 文件中有很多过滤配置,并且非常不利于后期维护。其实,在组件化中,我们通常都会在 base_module 上层再依赖一个 router_module ,这个 router_module 就是负责各个模块之间的页面跳转的

ARouter 源码理解

用过 ARouter的都知道,每个需要对其他 module 提供调用的 Activity ,在声明的时候都会带有 @Router 注解,我们称之为路由地址,如下所示:

1
2
3
4
@Router(path=/main/main)
public class MainActivity extends AppCompatActivity {

}

这个注解有什么用呢?路由框架会在项目编译期通过注解处理器扫描所有添加@Router注解的类,之后,将Router 注解中的 path 地址 和 Activity 的 class 文件映射关系保存到它自己生成的 java 文件中,示例如下:

1
2
3
4
public static HashMap<String, ClassBean> getRouteInfo(HashMap(String, ClassBean) routes) {
routes.put("/main/main", MainActivity.class);
routes.put("/login/login", LoginActivity.class);
}

这样,我们想在app 模块之间跳转,就可以通过这个映射关系找到目标了,一般使用方法如下:

1
2
3
4
ARouter.getInstance().build("/login/login")
.withString("password", 666666)
.withString("name", "小三")
.navigation();

上述例子中,通过 /login/login 可以找到对应的class : LoginActivity.class ,最终会通过 ActivityCompat.startActivity() 方式启动目标Activity。

ARouter在初始化的时候只会一次性地加载所有的Root结点,而不会加载任何一个Group结点,这样就会极大地降低初始化时加载结点的数量。那么什么时候加载分组结点呢?其实就是当某一个分组下的某一个页面第一次被访问的时候,整个分组的全部页面都会被加载进去,这就是ARouter的按需加载。其实在整个APP运行的周期中,并不是所有的页面都需要被访问到,可能只有20%的页面能够被访问到,所以这时候使用按需加载的策略就显得非常重要了,这样就会减轻很大的内存压力。

其他细节略

以上内容参考自玉刚说上的文章(如果玉刚说的文章么有了可以参考简书上的原文)、CSDN上的博客

系统架构

分为五层,从上往下依次是应用层、应用框架层、系统运行库层、硬件抽象层和Linux内核层。

  • 应用层(System Apps):系统内置的应用程序以及非系统级的应用程序都属于应用层,负责与用户进行直接交互。
  • 应用框架层(Java API FrameWork):应用框架层为开发人员提供了开发应用程序所需要的API,我们平时开发应用程序都是调用这一层提供的API。
  • 系统运行库层(Native):系统运行库层分为两部分:分别是C/C++程序库和Android运行时库。运行时库又分为核心库和ART(Dalvik),核心库提供了Java语言核心库的大多数功能,这样开发者可以使用Java语言来编写Android应用。
  • 硬件抽象层(HAL):硬件抽象层是位于操作系统内核与硬件电路之间的接口层,其目的在于将硬件抽象化,为了保护硬件厂商的知识产权,它隐藏了特定平台的硬件接口细节,为操作系统提供虚拟硬件平台。
  • Linux内核层(Linux Kernel):Android 的核心系统服务基于Linux内核,在此基础上添加了部分Android专用驱动。

Android系统源码目录

Android 8.0.0系统根目录结构说明如下图所示:

Android系统目录-1

Android系统目录-2

应用层

应用层位于整个Android系统的最上层,开发者开发的应用程序以及系统内置应用程序都在应用层,它的目录结构如下:

应用层目录-1

应用层目录-2

应用框架层部分

应用框架层是系统的核心部分,一方面向上提供接口给应用层调用,另一方面向下与下层进行衔接,应用框架层的主要实现代码在 frameworks/base 和 frameworks/av 目录下,其中frameworks/base 目录结构如下:

应用框架层目录

源码阅读

系统源码阅读的方式有在线阅读和下载源码到本地用软件工具阅读。

在线阅读

Android 在线阅读源码的网站有很多,比如:http://androidxref.com/,http://www.androidos.net.cn/,在书中,作者推荐使用前一个url在线阅读;我个人比较推荐第二个,因为它在类搜索这块做得非常舒服,像使用google、baidu 一样容易使用,此外,它还是中文界面。

使用Source Insight

下载源码到本地,再用软件工具阅读源码是最好的方式,不受网速影响,效率更高,本地阅读源码可以采用 Android Studio、Eclipse、Sublime 和Source Insight 等软件,这里推荐使用Source Insight 。

需要注意的是,Android中的Dalvik 和 ART 并不属于Java 虚拟机。

当一个Java 文件经过Java编译器后会生成Class 文件,这个Class 文件会由 Java 虚拟机进行处理,Java 虚拟机与 Java 语言没有必然的联系,它只与特定的二进制文件:Class 文件有关。任何语言只要能编译成 Class 文件,都能被Java 虚拟机识别并执行。

Java虚拟机结构

按照Java虚拟机规范,抽象的Java 虚拟机如下图所示:

Java虚拟机结构

从图可以看出,Java虚拟机结构包括 运行时数据区域、执行引擎、本地库接口和本地方法库,其中类加载子系统并不属于Java 虚拟机的内部结构。下面针对这个图来介绍Android 开发需要掌握的Class 文件格式和运行时数据区域。

类的生命周期

类的生命周期如下图所示:

类的生命周期

类加载的各个阶段的工作:

  1. 加载: 查找并加载Class文件

  2. 连接: 包括验证、准备和解析

    • 验证:确保被导入类型的正确性
    • 准备:为类的**静态字段分配字段,并用默认值初始化这些字段
    • 解析:虚拟机将常量池内的符号引用替换为直接引用
  3. 初始化: 将类变量初始化为正确的初始值。

运行时数据区域

可取的几点:

  • 为了在线程切换后能恢复到正确的执行位置,每个线程都会有个独立的程序计数器。如果线程执行的方法是Native方法,则程序计数器的值为空(Undefined),否则,保存正在执行的字节码指令地址。
  • 可以选择在方法区不显示垃圾收集

其他的可以参考深入理解Java虚拟机的这篇文章即可。

对象的创建

当虚拟机接收到一个 new 指令时,它会做如下操作:

  1. 判断对象对应的类是否加载、链接、以及初始化。
  2. 为对象分配内存:如果内存规整,则只需要将指针指示器向空闲的内存移动若干距离即可;若不规整,则需要由虚拟机维护一个列表记录哪些内存是可用的。
  3. 处理并发安全问题,有两种方式解决:
    • 对分配内存空间的动作做同步处理,比如采用 CAS 方式配合失败重试
    • 为每个线程在Java堆中预先分配一小块内存,只有这块内存被使用完后,才需要同步的方式分配新的内存。
  4. 初始化分配到的内存空间:将分配到的内存,出对象头以外都初始化为零值。
  5. 设置对象的对象头:将对象所属的类、对象的hashcode、以及GC分代年龄等存放在对象头。
  6. 执行init方法进行初始化。

垃圾标记算法

GC 主要做两个工作:一是内存的划分和分配,二是对垃圾进行回收。关于内存的划分和分配,目前Java虚拟机内存的划分是依赖于GC 的,比如现在的GC 都采用分代收集算法来回收垃圾的。Java 堆作为GC 的主要管理区域,被细分为新生代和老年代。

查看gc日志的时候,[GC(Systemt.gc()) 和 [Full GC(Systemt.gc() 用于说明此次垃圾收集的停顿类型(而非区分新生代和老年代),后者表示此次GC发生了 STW(Stop the World,只有GC线程在运行,其他线程都暂停)。

其余主要内容,参考以前写的这篇文章

垃圾收集算法

标记-清除算法

首先标记可以被回收的对象,之后回收被标记的对象所占的空间。它主要有两大缺点:一个是标记和清除两个过程的效率都不高,另一个就是,容易产生大量的内存碎片,碎片太多可能会导致没有足够的连续内存分配给较大的对象,从而提前触发新的GC。

复制算法

把空间分为两个相等部分,每次只使用其中一部分,垃圾收集时,把存货的对象复制到另一部分,再把当前部分全部清理掉,这样不需要考虑碎片化问题,缺点就是可用内存变为了原来的一半。

标记-压缩算法

新生代一半存活的对象比较少,可以使用复制算法,但是老年代不能选择复制算法了,因为老年代的对象存活率会较高,这样会有很多复制操作,导致效率降低。它的主要方式是,在标记可回收的对象后,将所有还存活的对象压缩到内存的一端,使它们紧凑地排列在一起,然后对边界外的内存进行回收。标记-压缩算法解决了标记-清除算法容易产生大量内存碎片的问题,但是它的效率仍然是很低的。

分代收集

垃圾收集的类型分为两种,分别是:

  • Minor Collection: 新生代垃圾收集
  • Full Collection: 老年代垃圾收集,也称 Major Collection, Full Collection 通常会伴随至少一次的 Minor Collection。 Full Collection 收集频率低,耗时长。

LeetCode-剑指offer系列


面试题03、数组中重复的数字

点击看答案

HashSet 是否添加进去了

自己写的时候的问题:没啥,用hashset 搞定,但是忘了 new 关键字。。。

力扣

面试题04、二维数组中的查找

点击看答案

左下角标志法

自己写的时候的问题:自己写得完美,没问题。为了加深印象,我们说行数 rowNum = matrix.length; columnNum = matrix[0].length, 之后从左下角开始即可。

力扣

面试题05、替换String空格

点击看答案

使用 StringBuilder

自己写的时候的问题:没啥问题,StringBuilder 疯狂拼接即可

力扣

面试题10-I、斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出,求第n个数,但是结果要对 1000000007,如计算初始结果为:1000000008,请返回 1。

点击看答案

使用 StringBuilder

自己写的时候的问题:大体上使用数组是对的,但是在做计算的时候写成了: arrs[i] = arrs[i - 1]%mod + arrs[i-2] % mod;,而实际上应该写成: arrs[i] = (arrs[i - 1] + arrs[i-2]) % mod; 以后要注意

力扣

还可以延伸青蛙跳台阶问题

面试题11、旋转数组的最小数字

点击看答案

二分法

自己写的时候的问题:没啥难,就是要注意细节,1、没有旋转的情况 2、所有数字都相同的情况 3、只有一个数字的情况 4、剩下的就是从后面往前面找,当到某一个数字它的前面一个数字比当前数字大的时候,那最小数字就是当前数字了。

力扣

面试题15、二进制中1的个数

点击看答案

n & (n -1)

自己写的时候的问题:没啥,还是 n & (n-1)

力扣

面试题18、删除链表的节点

点击看答案

两个指针

自己写的时候的问题:没啥,处理好head,之后就是常用的pre 指针、current 指针了

力扣

面试题21、 调整数组顺序使奇数位于偶数前面

点击看答案

首尾双指针

自己写的时候的问题:没啥,很顺利。双指针,快排思想

力扣

面试题22、 链表中倒数第k个节点

点击看答案

前后指针

自己写的时候的问题:没啥,快慢指针,快指针先走k步

力扣

面试题24、 反转链表。反转一个单链表,如输入: 1->2->3->4->5->Null ,则输出:5->4->3->2->1->Null

点击看答案

存储当前节点和上一个节点

自己写的时候的问题:没啥,搞个pre

LeetCode

面试题25、 合并两个排序的链表

点击看答案

一个链表到表尾之后,需要接上另一个表的剩余部分

自己写的时候的问题:没啥,造一个空节点

力扣

包含min函数的栈

点击看答案

两个栈,一个维持压入数据,一个维持最小数栈,注意相同最小值要都压入最小数栈

力扣

面试题39、 数组中出现次数超过一半的数字

点击看答案

既然出现次数超过一半 那么排序后在中间的就是所需数字

自己写的时候的问题:先排序后取值不难,但是时间复杂度太高,提交的时候耗时1.3s;使用hashMap 计数,也不难,只是略微繁琐,还有 HashMap 额外占用空间。要熟练 摩尔计数法,这才是最优解。

力扣

面试题40、最小的k个数

点击看答案

大顶堆

自己写的时候的问题:暂时还没做

还可以引申数据流中中位数

力扣

连续子数组的最大和

点击看答案

要么取大值,要么另起炉灶

自己写的时候的问题:与之前写的 力扣53题的答案,这次更加简洁粗暴,直接判断result < 0 是否成立,成立就放弃result,否则就还让它继续 result += element;重点记住这个答案就好

力扣

面试题50、第一个只出现一次的字符

点击看答案

hashmap 存储次数

自己写的时候的问题:HashMap 辅助,没啥难度。在 字符出现次数 > 1 的时候,不要尝试去增加它的次数,节省put 操作。 因为 2次 和 100次 对我们意义是一样的

力扣

面试题52、两个链表的第一个公共节点

点击看答案

两个指针 node1,node2 分别指向两个链表 headA,headB 的头结点,然后同时分别逐结点遍历,当 node1 到达链表 headA 的末尾时,重新定位到链表 headB 的头结点;当 node2 到达链表 headB 的末尾时,重新定位到链表 headA 的头结点。这样,当它们相遇时,所指向的结点就是第一个公共结点

自己写的时候的问题:刚开始脑抽只允许一个转换,其实 应该 A 和B两个链表应该各有一次转换机会。

力扣

面试题53 - I、在排序数组中查找数字 I,统计一个数字在排序数组中出现的次数

点击看答案

二分法

自己写的时候的问题:自己写没问题,二分查找没毛病。

力扣

面试题53 - II. 0~n-1中缺失的数字。一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字

点击看答案

解法1:与下标是否相等 解法2:求和?(要考虑溢出) 解法3:二分法

自己写的时候的问题:在数组下标都对应得上的时候,缺失的就是最后一个数字,写的时候没想到这点。

力扣

面试题56 - I、数组中数字出现的次数。一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)

点击看答案

异或-> 数字分组

自己写的时候的问题:不太熟练,自己只能想到全员异或,然后分组,之后想不到了。其实在分组过后,每组元素都是由 若干个出现两次的数字和一个只出现一次的数字 组成,组内异或就能得到那个只出现一次的数字。

力扣

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素

点击看答案

全员异或

力扣

面试题57、和为s的两个数字。输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可

点击看答案

双指针即可

自己写的时候的问题:没啥说的,提交完美。双指针即可

力扣

面试题58 - I、翻转单词顺序。输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简单起见,标点符号和普通字母一样处理。例如输入字符串”I am a student. “,则输出”student. a am I”

点击看答案

两次翻转

自己写的时候的问题:这题目本身就有点坑爹,注意审题,因为它首尾的空格要求在结果中去掉,并且单词间有多个空格也只保留一个。还有,清空StringBuilder 可以使用 sb.setLength(0) 的方式。其他的倒是没啥。

力扣

面试题58 - II、 左旋转字符串。字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串”abcdefg”和数字2,该函数将返回左旋转两位得到的结果”cdefgab”

点击看答案

subString即可

自己写的时候的问题:没啥,StringBuilder 拼接,简单粗暴

leetcode

面试题59 - I、 滑动窗口的最大值。给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值

点击看答案

上一次的最大值是不是被滑过的位置

自己写的时候的问题:自己写,不难。充分利用好上一次的最大值即可

leetcode

面试题61、 扑克牌中的顺子。从扑克牌中随机抽5张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为 0 ,可以看成任意数字。A 不能视为 14

点击看答案

根据差值计算大小王是否够用就行

自己写的时候的问题:看懂原理,就不难(抄别人的): 如果我们能够知道 5 张扑克牌中的最大值 maxValue 和最小值 minValue ,那我们就知道,要使它为顺子需要 maxValue - minValue + 1 张牌,所以我们只需要计算最大最小值(0除外),然后通过上述规律来判断。当然,如果其中有重复的数字(0除外),那么肯定凑不齐了。所以我们关键是找最大最小值,以及判断是否重复。这里面的细节很多,比如boolean[] 用来表示元素是否重复的数组,它的index 不是 遍历牌的 i ,而是牌面值,即 nums[i]

leetcode

面试题62、圆圈中最后剩下的数字。0,1,,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字。求出这个圆圈里剩下的最后一个数字

点击看答案

约瑟夫环,记得公式就行(返回的是数组下标)

1
2
3
4
5
6
7
8
int cir(int n,int m){
int p=0;
for(int i=2;i<=n;i++)
{
p=(p+m)%i;
}
return p;
}

题目leetcode、解答参考csdn

面试题65、不用加减乘除做加法。写一个函数,求两个整数之和,要求在函数体内不得使用 “+”、“-”、“*”、“/” 四则运算符号

点击看答案

异或操作

自己写的时候的问题:凭记忆做出来了,后续还是要加强。 注意一点再java中不要做 <<< 操作,非法的。

leetcode

面试题66、给定一个数组 A[0,1,…,n-1],请构建一个数组 B[0,1,…,n-1],其中 B 中的元素 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法。

点击看答案

对称数组

自己写的时候的问题:似懂非懂

leetcode

66、加一,给定一个由整数组成的非空数组所表示的非负整数,在该数的基础上加一,最高位存放在数组收尾,数组中每个元素只存储单个数字,可以假设除了0之外,这个整数不会以0开头。假如输入:[1,2,3] ,代表123,则输出 [1,2,4];输入 [4,3,2,1] ,代表 4321,则输出 [4,3,2,2]

点击看答案

它只是加一的话,可能的情况只有两种:

  • 除9之外的数字加一
  • 数字9加一
    加一得十,进一位,个位数为0;如加法运算不出现进位,则运算就结束了。还有一种情况就是 当出现 9,99,999 之类的数字时,循环到最后也需要进位,需要手动将它进一位

自己写的时候的问题:没啥问题,遍历每个元素让每个元素 +1 ,当某个元素 +1 之后 < 10,就将当前数组返回;否则,加到最后也还没返回,就要新建一个数组,最开始位为1

LeetCode