0%

对象已死吗

引用计数法

很多教科书判断对象是否存活的算法是这样的:给对象中添加一个引用计数器,每当一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。客观地说,引用计数法的实现简单,但它很难解决对象之间相互循环引用的问题,所以,主流实现中,并不使用这种方式。

可达性分析

在主流实现中,都是通过可达性分析来判定对象是否存活。这个算法的基本思路就是通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到GC Roots 没有任何引用链相连时,则证明此对象是不可用的。在Java语言中,可以作为 GC Roots 的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 本地方法栈中JNI引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。

再谈引用

JDK 1.2 以前,对象只有引用和被引用两种状态,未能描述那些“食之无味,弃之可惜”的对象,我们希望描述这样一类对象:当内存还足够时,则能保留在内存之中;如果内存空间在GC后还是非常紧张,则可以抛弃这些对象。因此,JDK 1.2以后对引用的概念进行扩充,分为:

  • 强引用。代码中普遍存在的,类似于 Object obj = new Object() ,只要强引用还在,垃圾收集器永远不会回收被引用的对象。

  • 软引用。是用来描述一些还有用,但是并非必需的对象,在系统将要发生内存溢出的异常之前,将会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。

  • 弱引用。是用来描述非必需对象的,但是强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次GC发生之前。当GC工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

  • 虚引用。是最弱的一种引用关系,不会对对象的生存时间构成影响,也无法通过虚引用获得对象实例。为对象设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

生存还是死亡

即使在可达性分析算法中不可达的对象,也并非“非死不可”的。要真正宣告一个对象的死亡,至少要经历两次标记过程:如果对象在可达性分析后发现没有与GC Roots 相连接的引用链,那么它会被第一次标记,并进行筛选,筛选的条件是此对象是否有必要执行 finalize() 方法,当对象没有覆写 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,则都没有必要执行;

如果对象判定有必要执行 finalize() 方法,则对象会被放入F-Queue队列中,稍后由低优先级的线程去触发 执行finalize() 方法,但不会承诺等待它运行结束。这样做的原因是防止 finalize 方法过于缓慢或者死循环。finalize() 方法是对象套多死亡命运的最后一次机会,因为稍后GC将对F-Queue中的对象进行第二次标记,如果对象要在finalize 中拯救自己————只要重新与某个引用链上的任意一个对象关联即可,那么在第二次标记的时候它将被移除出“即将回收”的集合;如果这时候对象还没逃脱,那它就会真的被回收了。

说明这一过程的示例代码如下:

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 class FinalizeEscapeGC{
public static FinalizeEscapeGC SAVE_HOOK = null;

public void isAlive(){
System.out.println("yes,i am still alive");
}

@Override
protected void finalize() throws Throwable{
super.finalize();
System.out.println("finalize method executed");
FinalizeEscapeGc. SAVE_HOOK = this;
}


public static void main(String[] args) throws Throwable{
SAVE_HOOK = new FinalizeEscapeGC();

//对象第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
//因为finalize方法优先级很低,所以暂停0.5秒等待它
Thread.sleep(500);
if(SAVE_HOOK != null){
SAVE_HOOK.isAlive();
}else{
System.out.println("no,i am dead");
}


//下面这段代码与上面的完全相同,但是这次自救却失败了
SAVE_HOOK = null;
System.gc();
//因为finalize方法优先级很低,所以暂停0.5秒等待它
Thread.sleep(500);
if(SAVE_HOOK != null){
SAVE_HOOK.isAlive();
}else{
System.out.println("no,i am dead");
}
}
}

运行结果:

finalize method executed
yes,i am still alive
no,i am dead

从结果可以看出,SAVE_HOOK 对象的 finalize 方法确实被GC收集器触发过,并且收集前成功逃脱。但两段完全一样的代码,第二次却被回收了,因为finalize 方法只会被自动调用一次,因此第二次不执行了,所以自救失败。

有些教材上认为 finalize 中适合做“关闭外部资源”之类的工作,在了解以上机制之后,可以认为这完全是一种自我安慰,finalize 能做的,try-finally 或其他方法能做得更好,更及时,所以完全可以忘记这个方法存在(它也仅仅只是Java诞生时讨好C/C++程序员的一个妥协)。

垃圾收集算法

标记-清除算法

最基础的收集算法是 标记-清除(Mark-Sweep) 算法,它分为“标记”和“清除”两个阶段:首先标记出需要回收的对象,在标记完成后统一回收被标记的对象。它的主要不足有两个:一是效率问题,标记和清除效率都不高;二是清除后会产生大量不连续的内存碎片。

复制算法

为了解决效率问题,复制算法出现了。它将可用内存分为大小相等的两块,每次只使用其中一块,当这块内存用完了,就将还存活的对象复制到另一块上,然后把当前内存空间一次清理掉。这样每次都是对整个半区回收,内存分配也不会存在碎片。缺点是可用内存缩小了一半,代价太高。

现在商业虚拟机采用这种方式回收新生代。一般将内存分为较大的 Eden 和两块较小的 Survivor 空间(HotSpot中 Eden 与 Survivor大小比例为 8:1),每次使用Eden 和其中一块 Survivor,当内存回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor,最后清理Eden和刚才用过的Survivor空间,因此,每次只有10%的内存“被浪费”。当然这是基于统计新生代的对象生命周期都很短,差不多只有10%的对象会存活,此外,如果超出10%的存活对象,Survivor空间不够时,需要依赖老年代的空间提供担保(Survivor空间存不下的对象直接通过担保机制进入老年代),这是该分配方式能够运行下去的保障。

复制收集算法在对象存活率较高时要进行较多的复制操作,效率将会变低,更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行担保,防止对象100%存活的极端情况。

标记-整理算法

根据老年代特点,有人提出另外一种标记-整理(Mark-Compact)算法,标记过程仍与“标记-清除”算法一样,但后续不是直接清理可回收对象,而是所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。标记整理算法的示意图如下:

标记-整理算法

关于怎么个移动法,书上没有说明,这里个人做个推测:

1、整个老年代的内存划分为 A 和 B 两个区域,区域 A 的大小是所有还存活对象的总大小,区域 B 大小是是剩余空间。
2、按顺序在 A 中找到第一个还存活对象,将它移动到 A 空间的最前端,接下来找第二个还存活的对象,将其移动到第一个对象的后面
3、以此类推,即使存活的对象在 B 中也一样顺序复制到A中,直至最后填满A空间,之后清除 B 空间。

分代收集算法

商业虚拟机都采用“分代收集”,根据各个年代的特点分别采用适当的收集算法:

  • 在新生代中,每次垃圾收集时都会发现有大批对象死去,只有少量存活(前面提到的统计数据,大部分时候只有 10% 存活),那就选用复制算法。
  • 而老年代中因对象存活率高,没有额外空间对它进行担保,就必须使用“标记-清理”或者“标记-整理”算法进行回收。

CMS 收集器

这是自己添加的这个标题,书中的结构并不是如此,因为 Android ART 虚拟机使用的也是CMS收集方式,所以这里特意抽出来理解,方便理解Android的虚拟机。 CMS (Concurrent Mark Sweep) 收集器,并发的“标记-清除”方式实现的收集器。它是一种以获取最短回收停顿时间为目标的收集器。它的整个过程分为4个步骤:

  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清除

其中,初始标记、重新标记 这两个步骤仍然需要“Stop The World”,初始标记仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快;并发标记就是进行 GC Roots Tracing 的过程(根据初始标记过程中标识出来的直接关联对象,并发标记存活的对象);重新标记阶段则是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录;

由于整个过程中好事最长的 并发标记 和 并发清除 过程收集器线程都可以与用户线程一起工作,所以,从总体上来讲,CMS收集器的内存回收过程是与用户线程一起并发执行的。

CMS 是优秀的收集器,但是它也有3个明显的缺点:

  • CMS 收集器对 CPU 资源非常敏感。与其他并发设计的程序一样,CMS收集器对 CPU 同样敏感,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用变慢。

  • CMS 无法处理浮动垃圾(Floating Garbage,个人认为简而言之就是GC过程中用户线程产生的垃圾,类比于你妈妈在打扫房间卫生的时候,你还在继续撕的纸)。

  • 还有最后一个缺点,就是CMS本质还是“标记-清除”算法实现的,这种收集方式在结束后会产生大量的碎片化的空间。

内存分配与回收策略

以下是几条最普遍的内存分配规则,可以通过代码去验证这些规则:

对象优先在 Eden 上分配

大多数情况下,对象在新生代 Eden 区中分配,当 Eden 区没有足够空间时,虚拟机将发起一次 Minor GC (新生代GC)。可以采用以下方式验证:

通过 -Xms20M、-Xmx20M、-Xmn10M 三个参数限制Java 虚拟机堆大小 20MB,不可扩展,并且其中 10M 分配给新生代,剩下的 10MB 分配给老年代,-XX:SurvivorRatio=8决定新生代中 Edun 区与 一个 Survivor区的比例是 8:1,即 Eden 大小 8192K,两个 survivor 空间分别 1024K,所以新生代总共可用空间是 9216K (Eden 区 + 1个Survivor区)。
执行 testAllocation()尝试分配 3个 2M 的大小和1个 4MB 大小的对象。
代码执行完成后,可以发现 新生代Eden区被占用了 4M,而老年代被占用了 6M。
解释:分配前3个对象时,没有压力,均分配在Eden区域,当分配第4个对象时,内存已经不够了(可用空间是 9216K,而 2M*3 + 4M = 10),因此触发一次Minor GC,此时发现这3个对象都存活,并且 survivor 区间不够容纳存活的对象,因此这3个对象被直接通过担保机制提前转移到老年代中;这次GC结束后,第4个对象就可以顺利分配到Eden空间中。

新生代GC(Minor GC):发生在新生代的垃圾收集,因为Java对象大多朝生夕死,所以Minor GC非常频繁,一般回收速度也比较快
老年代GC(Major GC/Full GC):指的是老年代的GC,对于许多收集器而言,这时候都会伴随至少一次的 Minor GC,老年代的GC一般会比 Minor GC 慢10倍以上。

大对象直接进入老年代

所谓大对象是指需要大量连续内存空间的Java对象,最典型的就是很长的字符串以及数组。当然,比大对象更糟糕的是遇到一群朝生夕死的“短命大对象”,这种应当避免。经常出现大对象容易导致内存还有不少空间就提前触发垃圾收集器以获得足够的连续空间来安置它们。

长期存活的对象进入老年代

如果对象在Eden出生并经历过第一次Minor GC 后仍然存活并且能被 Survivor 容纳的话,将被移动到Survivor 中,并且年龄设置为 1,此后,每熬过一次 Minor GC ,对象的年龄就加1,当达到某个阈值(系统一般默认 15),就会被晋升到老年代中。

动态对象年龄判定

为了更好地适应不同程序内存状况,并不总是要求对象必须达到某个年龄才进入老年代。如果 Survivor 中同龄对象达到某个阈值(一般是一半)时,大于或者等于这个年龄的对象就会移动到老年代。

空间分配担保

发生Minor GC 前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立,则 Minor GC 可以确保是安全的,如果不成立,则会判断是否允许担保失败,如果允许担保失败,则最终可能会触发一次Full GC。虽然绕了一圈还是有可能触发 Full GC,但是这种情况相对较少。

运行时数据区域

根据《Java虚拟机规范(java SE 7)》规定,Java 虚拟机所管理的内存将会包括以下几个运行时数据区域:

Java运行时数据区域

程序计数器

程序计数器可以看做是当前线程执行的字节码的行号指示器,它是线程私有的。在虚拟机概念模型里(具体虚拟机可能有更高效实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖程序计数器来完成。如果线程在执行Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行Native方法,这个计数器值为Undefined。此内存区域是Java虚拟机规范中唯一一个没有规定任何 OutOfMemoryError情况的区域

Java虚拟机栈

Java虚拟机栈也是线程私有的。它描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,一个方法从调用至完成,就对应一个栈帧在虚拟机栈中入栈到出栈的过程。这个区域被规定了两种异常状况:如果线程请求的栈深度大于虚拟机允许的深度,就抛出 StackOverflowError异常;如果该区域可以动态扩展,则在扩展时无法申请到足够内存,就会抛出OutOfMemoryError异常

本地方法栈

本地方法栈与虚拟机栈类似,区别只不过是虚拟机栈为执行Java方法(即字节码)服务,而本地方法栈为Native方法服务,同样,本地方法栈也可能抛出StackOverflowError异常以及OutOfMemoryError异常.

Java堆

Java堆是被所有线程共享的一块内存,虚拟机启动时创建。虚拟机规范描述是,所有对象的实例以及数组都要在堆上分配(当然,不同虚拟机实现不同)。如果堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常

方法区

与Java堆一样,方法区也是各个线程共享的内存区域。它用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。当方法区无法满足内存分配需要时,将抛出OutOfMemoryError异常

提到方法区,不得不提运行时常量池,它是方法区的一个部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是信息常量池,用于存放编译期生成的各种字面量和符号引用(一般来说,翻译出来的直接引用也会存储在运行时常量池中)。

直接内存

直接内存(Direct Memory)不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。在Java 1.4中新加入了NIO(New Input/Output)类,它可以使用Native函数直接分配堆外内存,然后通过一个存储在Java堆中的DirectBuffer对象作为这块内存额引用进行操作,这样能在一些场景中显著提高性能,因为它避免在 Java 堆和 Native 堆中来回复制数据。当然,该区域空间在动态扩展时也可能出现OutOfMemoryError异常

HotSpot 虚拟机对象探秘

对象的创建

虚拟机遇到一条new指令时,首先检查这个指令的参数是否能在常量池中定位到类的符号引用,并检查这个符号引用代表的类是否已经被加载、解析和初始化过,如果没有,则必须限制性相应的类加载过程。类加载完成后,便可完全确定对象所需的内存大小,接下来为新生的对象真正分配内存。

对象的内存布局

对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)、和对齐填充(Padding)。

对象的访问定位

建立对象是为了使用对象,我们的Java程序需要通过栈上的refrence数据来操作堆上的具体对象。目前主流的访问方式有使用句柄和直接指针,其中前者通过句柄池间接指向堆中的对象,后者直接指向堆中的对象。

实战

OutOfMemoryError异常

Java虚拟机规范中,除了程序计数器之外,其他区域都可能发生 OutOfMemoryError异常。

JAVA堆溢出

Java堆用于存储实例对象,只要不断地创建对象,并且保证GC root 到对象之间有可达路径来避免垃圾回收,那么就能产生OutOfMemoryError异常。

虚拟机栈和本地方法栈溢出

虚拟机栈和本地方法栈理论上分配不到足够的内存同样会报 OutOfMemoryError异常,但是一般情况下,首先会出现StackOverflowError,OutOfMemoryError很难出现。

方法区和运行时常量池溢出

由于运行时常量池是方法区的一个部分,因此这两个区域的溢出测试放在一起进行。在此之前我们先了解下String.intern()方法:

String.intern()是一个Native方法,它的作用是,如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并返回此String对象的引用。

在JDK1.6及以前,通过设置永久代区域的大小可以间接限制方法区大小,但是从JDK 1.7以后就“去永久代”了,因此以下代码只在JDK 1.6 ,并且设置了 MaxPermSize 时有效:

1
2
3
4
5
6
7
8
public static void main(String[] args){
//使用List保持常量池的引用,避免full gc 回收常量池
List<String> list = new ArrayList<String>();
int i = 0;
while(true){
list.add(String.valueOf(i++).intern());
}
}

同样的原因,还可以引申一个更有意思的影响,如下代码:

1
2
3
4
5
6
7
public static void main(String[] args){
String str1 = new StringBuilder("计算机").append(“软件”).toString();
Systemt.out.println(str1.intern() == str1);

String str2 = new StringBuilder("ja").append("va").toString();
Systemt.out.println(str2.intern() == str2);
}

这段代码在JDK 1.6 中运行,会得到两个false(intern 方法会把首次遇到的字符串实例复制到永久代中,而StringBuilder创建的字符串实例在Java堆上,所以必然不是同一个引用);而在JDK 1.7 中会得到一个true(intern 实现不会再复制实例,只是在常量池中记录首次出现的实例引用,因此,intern返回的引用和由StringBuilder创建的字符串实例是同一个实例)一个false(“java”字符串在执行StringBuilder.toString()之前已经出现过,字符串常量池中已经有它的引用了,不符合首次出现,而“计算机软件”这个字符串则是首次出现的—这个解释没看懂,需要再次理解)。

本机直接内存溢出

直接或者间接地使用NIO,就可能出现本机直接内存溢出。虽然使用DirectByteBuffer分配内存也会抛出内存溢出异常,但是抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配。因此可以通过以下代码手动抛出异常,真正抛出异常的地方是unsafe.allocateMemory()。

1
2
3
4
5
6
7
8
9
10
11
//以下代码能够抛出异常的前提是,设置虚拟机参数 -XX: MaxDirectMemorySize 的值

private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws Exception{
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true){
unsafe.allocateMemory(_1MB);
}
}

704、二分查找,给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1

点击看答案

还没写。。。。。

自己写的时候的问题:这题目自己写得还不错

参考链接:LeetCode

进程间通信的经典实现

共享内存

共享内存的工作流程:

  1. 创建共享区。进程1从内存中申请一块内存作为共享区域,并且将该区域与某个Key绑定
  2. 映射共享区。创建共享区之后,需要将其映射到进程1的空间中才能操作。
  3. 访问共享区。进程2利用第一步得到的Key,访问到共享区,之后将这块内存映射到进程2的空间中。
  4. 进程间通信。各进程实现了对共享内存的映射后,便可以利用该区域进行信息交换。(注意:整个内存过程都没有实现同步机制,需要进程间自己实现)

管道

  • 进程A和B分立管道两端,进行数据传输通信
  • 管道是单向的,如果进程既要“读”也要“写”,就需要两根管道这点很像水管的特性
  • 管道同时具有“读取”(read end)端和“写入”(write end)端,比如进程A从 write end 写入,进程 B 就可以从 read end 端读取到数据
  • 管道容量有限制,当pipe满了后,写操作将阻塞;反之,pipe空了之后,读操作将会阻塞
  • 普通的管道是匿名的,这种匿名的管道只适合有斧子关系的进程通信,如果要实现没有任何关系的管道间的通信,就需要命名管道(Named pipe)

UNIX DOMAIN SOCKET

  • 简称UDS,也可以说是IPC Socket,与普通的Socket不一样,专门针对单机进程间通信提出来的。
  • 大家熟知的 Network Socket 是以 TCP/IP 协议栈作为基础的,需要分包、重组 等一系列操作,而 UDS 因为是本机内的“安全可靠操作”,实现机制上并不依赖于这些协议。
  • Android 中使用最多的一种IPC就是Binder,其次就是 UDS 。

RPC(REMOTE PROCEDURE CALLS)

RPC 涉及通信双方通常运行于两台不同的机器中。

同步机制的经典实现

###信号量(SEMAPHORE)

信号量与PV原语是分不开的,也是最为广泛的互斥方法之一,主要包括以下几个元素:

  • Semaphore S(信号量,用于指示共享资源的可用数量)
  • Operation P (可以减小S计数)
  • Operation V (可以增加S计数)

P执行:S = S -1,此时判断若 S>=0 ,说明资源此时允许访问,开始操作共享资源;否则,加入等待队列,待别人释放资源后唤醒。
V执行:S=S+1,如果此时S>0,说明当前没有希望访问资源的等待者;否则,唤醒等待队列中的相关对象。
其中,PV源于都属于原子操作,意味着他们执行过程是不允许被中断的。

互斥体(MUTEX)

Mutex 是 Mutual Exclusion 的缩写,释义为 互斥体。如果资源允许多个对象同时访问,成为 Counting Semaphores;而如果只允许取值0或1(lock/unlocked)的Semaphore,则叫做 Binary Semaphore。Binary Semaphore 与Mutex有相同性质,Mutex通常是对某一排他资源的共享控制——要么这个资源被占用(locked),要么是可以访问的(unlocked)。

管程(MONITOR)

针对信号量机制的程序易读性较差,并且信号量管理分散在各个参与对象中,很难维护等缺点,管程被提出来了。管程是可以被多个进程/线程安全访问的对象或者模块,管程中的方法在同一时刻只允许一个范文这使用它们(方法受 mutual exclusion 保护的)。

操作系统内存基础

操作系统中任何操作都与内存息息相关,内存管理的底层原理主要注意几个核心:

  • 虚拟内存

内存总是“不够大”的,随着应用的增加,总会填满,虚拟内存为运行更多的程序提供了可能,其基本思想是:1.将硬盘上一部分空间作为内存的扩展;2.出现资源不足时,按照一定算法挑出优先级较低的数据块移动到第1步划出的空间;3.需要用到硬盘中的数据块时,系统将产生“缺页”,之后把硬盘中的数据交换回内存中。

  • 内存分配与回收

分配、native层回收,java层回收

  • 内存保护

内存分页或者分段式管理。进程的逻辑地址不是直接对应物理地址的,因此没办法访问它范围外的内存空间。

顺带一提,mmap函数(Memory Map)正如其名,可以将某个设备或者文件映射到应用进程的内存空间中,这样访问这块内存就相当于对设备/文件进行读写;可见,理论上mmap可以用于进程间通信,即通过映射同一块物理内存来共享内存,这种方式因为减少了复制次数,在一定程度上提高了进程间通信效率。

Android 匿名共享内存(Ashmem)

Ashmem 全称 Anony Shared Memory,是Android 特有的内存共享机制,它可以将指定的物理内存分别映射到各个进程的地址空间,从而便捷地实现进程间的内存共享。Ashmem 是一个 misc 设备,其实现依托于 /dev/ashmem 。

根据技术栈可能的提问

1、聊聊 java 中 static 关键字

点击看答案

一旦什么东西设置为 static ,数据或者方法就不会同那个类的任何对象实例有联系。例如以下类:

1
2
3
class StaticTest{
static int i = 47;
}

尽管我们可以给StaticTest 类new 出 2个对象来,但是 StaticTest.i 仍然只有一个存储空间,即两个对象共享同样的i,此时,其中一个对象执行 ++i 后,另一个对象的 i值 也会变为 48。

静态的变量或者方法,可以通过对象引用,也可以直接通过类引用,如以上的i,可以使用如下两种方式引用:

假设st 是 StaticTest 类的对象: st.i = 4 或者 StaticTest.i = 4

2、Kotlin 相关

点击看答案

Kotlin 中的协程用过吗?聊聊?

简单使用过,但是仅仅用于线程切换,对协程有一些简单的了解:

  • 协程是编译器级别的,进程和线程是操作系统级的
  • 线程根据os的调度算法,当分配的时间片用完后,保存当前上下文,之后被强制挂起,开发者无法精确控制它们
  • 协程可以看做是轻量级的用户态线程
  • 协程实现的是非抢占式的调度,由当前协程控制什么时候切换到其他协程
  • 每个协程池里都有一个调度器,这个调度器是被动调度的,即当前协程主动让出cpu时调度
  • 目前的协程框架一般设计成 1:N 的模式,即一个线程作为容器,里面包含多个协程

优点

  • 协程轻量,创建成本小,内存消耗小

  • 协作式的用户态调度器,cpu上下文切换开销少

    进程/线程 切换需要在内核完成,而协程通过用户态栈实现,速度更快,但协程也放弃了线程中优先级的概念

  • 减少同步加锁,整体性能提高

    协程基于事件循环,减少同步加锁的频率。但若存在竞争,该上锁的地方仍需要加上协程锁

  • 可以按照同步思维写异步代码,即用同步的逻辑,写由协程调度的回调

    协程可以减少callback 的使用,但是不能完全替代callback,基于事件驱动的变成用callback更合适

缺点:

  • 协程中不能有阻塞操作,否则整个线程被阻塞(协程是语言级别,线程是操作系统级别)
  • 需要特别关注全部变量、对象引用的使用
  • 协程擅长处理IO密集型程序效率问题,但处理cpu密集型不是它的长处

    假设线程中有个协程是cpu密集型,但是没有io操作,也就是一时半会不会主动触发调度器调度,从而其他协程得不到执行

适用场景:

  1. 高性能计算,牺牲公平性换区吞吐量;
  2. 在 IO 密集型程序中。由于io密集型的程序中往往需要 CPU 频繁切换线程,带来大量性能浪费。但是协程可以很好地解决这个问题:比如把一个IO操作写成一个协程,当触发IO操作时就自动让出cpu给其他协程,协程间的切换是很轻的。
  3. 流式计算。消除Callback Hell。

Kotlin 优势

按照官网上的说法:

  • 简洁。语法简单,代码很少。判空、getter、setter 方法、命名传参(动态改变参数)无需重载,可能结合anko 之类的更加简单
  • 安全,减少空指针等错误、类型判断过后,自动类型转换
  • 兼容java,可以混编

缺点

  • lateinit,也容易引起空指针,即还未初始化
  • 直接使用 ArrayList 之类的list 是不能直接添加元素的,得使用 MutableList 才行
  • 引入了kotlin 支持库,apk包体积增加
  • 如果某个变量设置为可空的,那么即使你在初始化后,已经不空了,你也只能使用 ? 或者 !! 操作来使用它,感觉会有点乱

kotlin如何实现空安全

  • 可空类型和不可空类型
  • 使用 ? 进行安全调用
  • 入参可以指定可空和非空类型
  • 安全的类型转换,如 a as? Int
  • 可以方便过滤非空元素,如: val intList: List = nullableList.filterNotNull()

一定能避免空指针问题吗?我认为是不能,因为有 lateInit 变量存在,有可能这个变量还没初始化,就会导致是空的

3、有自定义view的经验,那如何理解 MeasureSpec?

点击看答案

MeasureSpec 的含义是:父View传递给当前 View 的一个建议值。MeasureSpec 是个int 类型的数字,转换成二进制后,前2位代表模式(mode),后30位代表数值(size)。模式总共分为3种:

MeasureSpec测量模式

measureSpec & MODE_MASK 即可获得mode的值;而 measureSpec & ~MODE_MASK 即可获得数值

那么,measureSpec 的值到底是如何计算得到的?view的 measureSpec 根据view 的布局参数(LayoutParams) 和 父容器的 MeasureSpec 值计算得到的,计算方法如下图所示:

view的measureSpec值

由于UNSPECIFIED模式用于系统内部多次measure 的情况(如listview、gridview 等),很少用到自定义view上,因此我们很少讨论。以下总结的规律也不讨论:

measureSpec规律

以上总结中,父容器的剩余空间指的是父容器除了padding之外的所剩余的空间,至于父容器的剩余空间与大小不超过父容器的剩余空间,看代码和看图都没能理解,后续再理解吧

以上内容部分参考自这个链接

4、聊聊 Android 中事件分发机制?

点击看答案

参考以前写的这篇文章

上面的文章中已经写得很明白了,但是需要重点再提一下的是,如果在 onTouchEvent 中不消耗事件,则在同一个事件序列中,当前View无法再次接到事件。

5、如何处理手势冲突?

点击看答案

有外部和内部两种方式处理手势冲突。

外部拦截:由上面的事件分配可知,点击事件都会经过父容器拦截处理,如果父容器需要此事件就拦截,否则此事件就不拦截,这样就可以解决事件冲突。外部拦截法需要重写父容器的onInterceptTouchEvent,比较符合事件分发机制。

这里要注意的是,还是上面的原则,在 onInterceptTouchEvent 中,首先是ACTION_DOWN 这个事件,父容器必须返回false,即不拦截,因为一旦拦截了 ACTION_DOWN ,后续的 ACTION_MOVE 和 ACTION_UP 都没法再传递给子view了; 接下来的内容辩证看待:ACTION_MOVE根据需要是否拦截;ACTION_UP 必须返回false,因为如果返回true,那么子view 是接受不到 ACTION_UP 事件,onClick 事件就无法响应。

内部拦截法

可以利用view事件分发的原则,在适当的地方拦截就行。

当然,也可以让父空间不拦截,如果是ViewGroup的话,可以在 onInterceptTouchEvent 方法中请求忽略外层容器拦截事件:getParent().requestDisallowInterceptTouchEvent(true) 。如果是View的话,那么把getParent().requestDisallowInterceptTouchEvent(true) 写在setOnTouchListener 方法中可能更合适。

以上内容参考自一骑绝尘前行的乌龟

6、如何优化App性能?

点击看答案

一、精简资源

  • lint检测,删除无用的资源

二、减轻Application的负担

  • 将非紧急操作,放在子线程中处理
  • 只在主进程中初始化app内容(因为接了百度地图等,会开启多个进程)

三、UI绘制优化

  • 布局优化,尽量使用 ConstraintLayout 减少布局层次(因为深度遍历)
  • 布局复用,比如底部的布局大体相似,都使用同一个 layout
  • 避免过度绘制。排查移除叠加的背景
  • 减少资源数目,因为shape很难复用,故shape换成 固定的控件: ShapedTextView、ShapedConstrainLayout 等
  • 提高显示速度

使用 viewstub 延后显示。

四、内存相关优化

一言以蔽之: 开源节流

  • webview 新进程
  • 检查内存泄漏(LeakCanery)
  • 正确地使用引用,尤其Activity的context(尽量替换成Application 的context,Activity 的Context 一律弱引用),以及强引用、弱引用、软引用的正确使用。
  • 使用正确的容器,比如避免自动装箱(使用SparseArray等)、避免hashmap内存浪费(使用ArrayMap等)
  • 枚举替换成注解。

五、cpu 相关优化

  • 解析缓存数据一律放在子线程处理
  • SharedPreference 存储json改动
  • webview预加载

六、网络优化

  • 域名替换成ip(选取响应速度最快的ip),避免劫持同时提升响应速度,webview 中的网络请求由网络框架接管。

七、结构

  • mvp

八、避开高峰

  • 不要同时,充分利用IdleHandler,快速滑动的时候不加载图片

具体优化方式:

1、内存从经常性的 380M 左右降低到 330M 的水平(adb shell dumpsys com.esun.ui,现在可以使用profile)
2、页面秒开(talkingdata数据显示,优化前88%左右,93%的收集数据显示1秒以内打开,从onCreate 到onResume)
3、过度绘制(优化前几乎所有主要页面都是红色-蓝、绿、粉、红 分别代表过度绘制 1,2,3,4 次,优化后基本上都是蓝绿,粉色的比较少,红色的可能只有极少数小块)
4、App启动速度加快,冷启动,从3.5秒左右降低到1秒左右(录屏,记录从启动到展示flash页面,多次时间取平均值)
5、网络连接,网络的错误率4%(按次数统计出的)左右,dns加速后,网络错误率基本上保持,主要集中在网络超时、网络无连接两种异常,其中网络超时占了40%左右

引申-adb shell dumpsys meminfo com.esun.ui 中各数据含义

点击看答案

Native Heap: Native对象malloc得到的内存
Dalvik Heap: Java对象new得到的内存
Dalvik Other: 类数据结构和索引占据的内存
Stack: 栈占用的内存(栈空间使用,如函数调用、局部变量等)
Pss Total: 在硬盘上实际占用的空间大小
Heap Size: Heap总共内存大小 = Heap Alloc + Heap Free, HeapSize 有限制,超出阈值就oom
Heap Alloc: 应用所有实例分配的内存,包括应用从Zygote 共享分享的内存(只是分配的虚拟空间,并没有实际占用,比如:new long[1024*1024],此时alloc就会新增了8M,但是由于没赋值,所以物理内存上并没有占用,如果针对每个元素赋值,则pss total 就会增加8M)
Heap Free: 堆空闲的大小
Objects: 统计App内部组件对象的个数,其中Views、ViewRootImpl以及Activities个数,在Activity的onDestroy之后应该都会清零,如果未清零,就可能发生了内存泄露

Private Dirty: 私有的脏内存页(还在使用中)
Private Clean: 私有的干净内存页(现在未使用了)
Private Dirty + Private Clean 便是应用曾经申请过的内存空间大小

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

7、引申-ArrayMap的原理、SparseArray原理

点击看答案

ArrayMap

ArrayMap 相对HashMap 而言是以时间换空间。它使用两个数组,一个整数型数组存储 key 的 hashCode,另一个Object[] 类型的数组存储 key-value键值对,如下图所示:

ArrayMap原理

这样的结构避免了为每个key创建额外的对象,也即避免了自动装箱(如需要将int包装成 Integer) ,每次put新元素时,key的hashcode 在hashCode的数组中按照顺序存储,object数组中存储key和value。查询元素时,首先获取key的hashCode,然后用二分法查找该hashCode 在第一个数组中的index,则在object 数组中,key的位置在 index<<1 处,而value在 index<<1 + 1 处,如果此时的key并不是当前的key,则认为发生了冲突,此时以该key为中心点,分别上下匹配,直到匹配到为止。

在插入删除元素时,由于是数组组织形式,因此需要移动相关的元素,因此效率并不高;但是在数据量相对较少的情况下(有些博客说是1000条数据以下),ArrayMap 带来的时间开销并不明显,但是节省的内存却十分可观。

SparseArray

SparseArray 用于key为int类型,value 为 Object 的情形,与HashMap 相比,它避免了Integer 自动装箱,并且没有依赖entry 数据结构,因此更高效。它的结构如下图所示:

SparseArray原理

因为key是int类型,所以也就不需要什么hash值来计算index了,只要int值相等,就是同一个对象。插入和查找也是二分法,所以原理与ArrayMap 基本上一致,所以不多说。为了提升性能,删除元素时,并不需要马上将元素置为空,而是先将其标记为一个需要删除的元素,等真正需要删除时,才清空处理。即如果要插入新数据,如果数组已经填满了,则尝试垃圾回收一下,把标记为DELETE 的对象回收,然后重新寻找key值对应的索引,并插入。

** 除了SparseArray 可以替代 HashMap<Integer,V>外,还有 SparseIntArray替换HashMap<Integer,Integer>、SparseLongArray替换HashMap<Integer,Long>、LongSparseArray 替换 HashMap<Long,V> **

以上内容可以参考这个链接

8、描述http 三次握手?为什么3次,2次或者4次不行?

点击看答案

首先,准确地说是TCP/IP三次握手。因为http本身是应用层协议,只是因为目前http的传输层确实是TCP/IP,所以可以这么说。但是http并不依赖于tcp/ip。

TCP发起连接的一方A,会随机生成一个32位的序列号,比如是1000,以该序列号为原点,对自己每个将要发送的数据进行编号,连接的另一方B会对A的每次数据进行确认,如果A收到B的确认编号是2001,则意味着 1001~2000 编号已经安全到达B。握手的示意图如下所示:

tcp三次握手示意图

所以我们可以总结,TCP 连接握手,握的是啥?其实就是告知双方数据原点的序列号。那为什么是3次握手呢?个人认为有两个原因:

  • 确认通信双方的 接收/发送 能力是正常的。第一次握手,B可以知道自己的接收能力、A的发送能力是正常的;第二次握手,A可以知道双方的收/发能力是正常的;第三次握手,B知道双方的收/发能力都正常。
  • 节省资源。我们知道,等3次握手结束后,服务端才给这条链接分配必要端口、缓存等资源。如果是2次握手,那么在收到客户端的请求后服务端就得分配资源了,如果第2次握手由于超时丢失,那么客户端会认为服务器还未响应,可能造成两端都在等。或者客户端等到放弃这次请求,而服务端之前分配的资源会被浪费。

因此,3次握手是必需的,更多的请求次数可以,但是浪费资源,没必要。

以上内容有部分是参考知乎中的内容

9、延伸-http 使用80端口,如果客户端一个tcp/ip在连接,那么就无法建立其他tcp/ip连接,因为80端口在占用?

点击看答案

不是的,80端口一般只是http应用的默认监听端口,就是说新的连接都是发送到80端口的。但是监听80端口的程序会给新建立的连接分配一个可用的端口,所以实际的这条连接可能是机那里在服务端的 10010端口,客户端的8888端口上的。而80端口会继续监听是否有新的连接到来。

10、描述4次挥手,3次行不行?为什么?

点击看答案

tcp/ip 是全双工的,client 端在没有数据需要发送给server的时候,就发送FIN 信号告知Server ,然后终止对server 的数据传输,但是server 可以继续对client 发送数据包,这时候就是4次来终止连接,过程如下图所示:

tcp的4次挥手

但是,如果Server 收到client 的FIN 包之后,再也没有数据要发给Client 了,那么对Client 的ack 包和 Server 自己的FIN 包就能合并成一个包发送出去,4次挥手就能变成3次挥手。

关于图中的 time_await ,它的作用主要是1、为实现TCP全双工连接的可靠释放;2、为使旧的数据包在网络因过期而消失。更详细的解释可以参考以前的这篇文章

11、了解哪些设计模式?写个单例模式?

点击看答案
  • 单例模式
  • 建造者模式
  • 工厂模式
  • 适配器模式
  • 装饰模式
  • 观察者模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//线程安全的单例模式代码
public class Singleton{
//注意 volatitle 关键字
private static volatitle Singleton instance = null;

//构造函数私有
private Singleton(){
}

public static Singleton getInstance(){
if (instance == null){
synchronized(Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}

return instance;
}

}

以下针对项目可能的提问

12、了解多线程使用,聊聊锁可以分为哪些种类

点击看答案

大体可以分为,这不全部指锁的状态,有的指锁的特性,有的指锁的设计:

  • 乐观锁/悲观锁
  • 公平锁/非公平锁
  • 偏向锁
  • 轻量级锁
  • 自旋锁
  • 可重入锁

具体可以参考以前写的这篇博客

13、引申-聊聊 HandlerThread

点击看答案

HandlerThread 继承了 Thread ,所以本质上是个workThread,只不过它带了个Looper,无需开发者自己去做Looper.prepare() 操作,可以看下其关键源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void run() {
mTid = Process.myTid();
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
}

所以我们在使用的时候,首先new 出一个对象来,接着就要执行其start() 方法,以便完成 Looper 的初始化,其中,notifyAll() 主要用于方法 getLooper() 中通知 Looper 已经准备好,唤醒wait:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Looper getLooper() {
if (!isAlive()) {
return null;
}

// If the thread has been started, wait until the looper has been created.
synchronized (this) {
while (isAlive() && mLooper == null) {
try {
wait();
} catch (InterruptedException e) {
}
}
}
return mLooper;
}

在使用完成之后,需要手动退出Thread:mHandlerThread.quit(); ,其原理不用写也知道:

1
2
3
4
5
6
7
8
public boolean quit() {
Looper looper = getLooper();
if (looper != null) {
looper.quit();
return true;
}
return false;
}

从以上原理我们可以知道,HandlerThread 的使用场景就是:需要在子线程执行耗时的,并且可能有多个任务的操作(每个任务都开线程导致线程太多啊),比如多个下载任务(非同一个任务多线程下载),还有一个典型例子就是IntentService。

14、延伸-IntentService

点击看答案

我们知道,IntentService 使用非常简单,不需要自己建立线程,执行完毕后也无需我们自己关闭Service,只需要专心在 onHandleIntent(Intent intent) 方法中实现逻辑即可。IntentService 使用工作线程逐一处理所有启动请求,如果不需要在Service中执行并发任务,IntentService 是最好的选择。至于如何做到的,我们只要看关键源码即可:

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
public abstract class IntentService extends Service {
private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}

@Override
public void handleMessage(Message msg) {
onHandleIntent((Intent)msg.obj);
stopSelf(msg.arg1);
}
}

@Override
public void onCreate() {
super.onCreate();
HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
thread.start();

mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
}

@Override
public void onStart(@Nullable Intent intent, int startId) {
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent;
mServiceHandler.sendMessage(msg);
}
}

IntentService onCreate 中创建了 HandlerThread 实例,mServiceHandler 创建时使用了 HandlerThread 的 Looper,这决定了最终业务是在HandlerThread 中的子线程中执行的,在 handleMessage 方法中看到了熟悉的 onHandleIntent 方法调用,待 onHandleIntent 执行完毕后,马上执行Service 的 stopSelf(msg.arg1) 关闭自己。

这里使用 stopSelf(msg.arg1) 而不是 stopSelf(),而msg.arg1 即 startId,而这个 startId 就是 onStartCommand(Intent intent,int flags,int startId) 的最后一个参数。我们知道多次调用startService 来启动同一个Service ,只有第一次会执行 onCreate ,但是会多次调用onStartCommand,以及onStart(所以IntentService 中,在onStart方法里面发送Message到Handler),并且每次 startId 并不相同,且都大于0。而stopSelf() 最终会调用 stopSelf(-1)

stopSelf(int startId) 中的startId 与 onStartCommand 的startId 是一一对应的关系,所以,当我们调用stopSelf(int startId)时,系统会检测是否还有其它startId 存在,有的话就不销毁当前service,没有的话则销毁。

所以,为什么是调用stopSelf 而不是调用 stopSelf(int startId),从上面的比较我们得出:这是为了提高 IntentService 的利用率,如果在 onHandleIntent 方法执行完毕前,又调用了startService 启动了同一个 IntentService ,那么我们就没必要销毁当前service了,直接继续使用当前service 对象执行任务即可,这样有利于减少对象的销毁及创建。再提及一句,由于是使用HandlerThread ,所以多个任务只能是串行方式依次执行。

以上内容参考俗人浮生 的博客,以及 IntentService 官方源码

15、volatile 关键字有了解吗?

点击看答案

讲解之前,首先了解 原子性、可见性 以及 有序性 的基本概念:

原子性-可见性-有序性概念

一言以蔽之,volatile 保证可见性、有序性,但是不保证原子性。

保证可见性:多个线程共享一个volatile变量k,如果一个线程在工作内存中修改k的值会立即刷新到主存,同时将其他线程中的该值设置成已过期,其他线程在下次使用k值时,需要从主内存刷新获取。在k值更改前就已经在使用的情形,比如k值在做加法的途中,如果k值改变,则是不受影响的,必须是下一次再次使用k的时候,才会从主存去刷新。还有要注意的是,子线程使用成员变量都会将变量从主存中拷贝一份,而不是直接使用。

保证有序性:我们知道为了提高性能,cpu或者编译器会对代码重排序,代码的执行顺序不一定和我们写的顺序是一致的,它们只保证最终结果一致。volatile 保证读/写volatile 属性时,其前面的代码必须已经执行完成,它后面的代码也不能排到前面来执行。

不保证原子性:也即前面提到的,比如在做加法途中,这个k值改变了,是无法改变正在做的加法中的k值的。这也是volatile 修饰变量并不是线程安全的原因。

如果还不太了解,可以参考以前写的这篇读书笔记、还可以参考这篇文章,讲得很透彻

16、什么是大接口?

点击看答案

大接口就是所看到的整个页面,都是由一个接口数据决定的。当时基于的背景有几个:

  • 这个行业决定,如果有需求,可能会要求某个版本不让用户使用了。
  • 还是行业决定,页面要求能灵活变动,随时可能某个模块没有了,或者某个tab没有了。
  • 减少接口数量,减轻后台压力,我们知道,频繁的、少量数据的接口请求对后台不友好,可能握手、header 等就能占用很大一部分资源。

怎么做的:

本地有若干指定的view映射,根据后台返回,可以动态添加这些view。一般view都是占满一行,左右两边的边距确定。

17、怎么防止劫持?

点击看答案

背景:当时有用户反馈,我们的 webview 打开慢,并且有时候弹出广告,可我们自己并没有添加广告,因此初步认定可能是运营商劫持,事实上我们在百度上搜索一下运营商劫持,就有一大把的搜索结果,看来并非我们一家。在这个基础上,分析应该是通过dns 污染导致的

所以解决方案就是不使用运营商的dns,而使用119(腾讯的 119.29.29.29) 和 114(114.114.114.114) 的dns,参考网上的方案,自己写了个实现。在获取到的ip 中,随机选中一个缓存起来,缓存有效时间为15分钟。

在 API 的http 请求中,拦截请求,查询是否缓存该host的ip,如果有,判断是否过期。如果没有缓存或者过期,则会通过上面步骤获取ip,并把host 换成ip直连。

针对webview的http 类型的get请求,在WebviewClient 的 shouldInterceptRequest 回调中(执行在子线程),使用自定义构建的网络请求(根据WebResourceRequest 的 url 以及 headers 构建 okhttp 的 okHttpRequest,其中headers也加入okHttpRequest 的headers 中)。该请求会在可能的情况下,将url替换成ip直连,获取结果后,自行重新组装 WebResourceResponse 对象return。

https的ip直连会碰到一些问题,具体可以参考别人的博客

18、一般走查哪些代码?

点击看答案

关键代码,比如容易出现死循环的重试机制、错误上报机制、安全检测机制

19、如何文档归档

点击看答案

使用wiki,wiki内容包括:

  • 后端接口以及参数说明
  • 前端支持的协议以及支持的格式
  • 关键逻辑的边界和参数,如自动登录尝试的次数,防止出现死循环;网络超时时长

其他部门做业务的时候只需要看wiki就行,不用找具体的技术人员查看客户端代码

20、聊聊这个内部sdk的设计?

点击看答案

以前没有做过sdk,貌似也没地方可以参考,还有时间也很紧急,所以在技术上直接采用500里面的技术,并没有什么新意,做完之后,有小需求做的同时慢慢重构,自己得出一些经验吧:

  • 控制调用权限。只暴露几个类给用户即可,其余的类不允许用户调用。
  • 确定回调方式。1、调用接口中需要传递 activity,业务中使用用户的activity 执行activityForResult 接受业务返回数据。 2、如果使用广播,则使用本地广播
  • 防止资源名称和宿主app冲突,资源名称添加特定前缀
  • 传入的参数各种各样,需要注意判空、检验数据格式合法性等
  • 尽量不使用第三方的类库,目前sdk中使用第三方类库,接sdk的时候要求用户添加依赖

21、最有成就感的项目?最棘手的问题?

点击看答案

最有成就感的可能就是xx app吧,接触得比较多,虽然目前的流畅度还是一般般,但是做了比较多的努力:

  • 性能优化
  • 大接口试验
  • 在以前的基础上动过网络框架和图片框架
  • 在安全上也做了一些努力

22、引申-如何重新设计网络框架?

点击看答案
  • 调用方式改变,不需要传递2次的 responseClass
  • 采用kotlin 的线程调度(GlobalScope.launch()) 而不是rxjava 进行线程间的切换

23、引申-安全做了哪些努力

点击看答案
  • 在native层做app签名校验
  • 广播统一改为本地广播
  • LeakCanary防止内存泄漏
  • SharedPreference加密
  • allowBackUp = false
  • 某些key生成在native代码中做
  • https证书本地验证。

24、讲一讲你看过的第三方框架的源码?

点击看答案

可以讲讲 LeakCanary 和 阿里的框架 alpha

25、逛哪些论坛?

点击看答案

有逛csdn,gank.io(不过gank.io有时候更新得比较慢),apkbus,androidweekly.cn 啊 等等。

26、平时关注什么技术?

点击看答案

目前关注的就是 flutter 了

27、有什么想问我的

点击看答案

如果是hr初面:

这个岗位是新开设的还是原岗位人离职了?
这个岗位可以为公司带来什么价值?
想了解以下公司的培训机制和学习机制

如果是技术人员:

你觉得我能胜任这个职位吗(看这一关是否通过了)?
感觉不好,就问:你觉得我还有哪些不足?

终面的话:

如果顺利,问下部门、公司的发展啦
如果觉得基本上没戏了,就问下自己的缺陷在哪
如果模棱两可,问下一步流程是怎么样的

冒泡排序算法

点击看答案

思路:冒泡排序基于交换排序思想。依次比较相邻的两个数,将小数放在前面,大数放在后面。

即第一趟:首先比较第1个和第2个数,将小数放前,大数放后。然后比较第2个数和第3个数,将小数放前,大数放后,如此继续,直至比较最后两个数,将小数放前,大数放后。之后便是重复第一趟步骤,直至全部排序完成(或者说直到某一趟没有发生交换的时候)。每一趟完成后,最后一个数肯定是最大的那个数,所以一次for循环后,会有 len – 操作,即每趟都比上一趟少比较一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    private static void bubbleSort1(int[] ints) {
int len = ints.length;
boolean flag = true;
while (flag) {
flag = false;
for (int i = 1; i < len; i++) {
if (ints[i - 1] > ints[i]) {
int temp = ints[i];
ints[i] = ints[i-1];
ints[i-1] = temp;
flag = true;
}
}
len -- ;
}
}

冒泡排序在数据有序的情况下,只需要一趟即可,时间复杂度是 O(n),在最差的情况下,每趟都有比较,时间复杂度是 O(n^2) ,平均复杂度是 O(n^2),适合数据量较小的情况,它是稳定的排序方法,

选择排序

点击看答案

选择排序的基本思想:每次从待排序的数据元素中选出最小(大)的一个元素,放在序列的起始位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void selectSort(int[] a){
for (int i = 0; i< a.length - 1; i++){

int index = i;//当前趟最小的数所在index

for (int j=i+1; j < a.length; j++){
if (a[j] < a[index]){
index = j;
}
}

if (index != i){
int temp = a[i];
a[i] = a[index];
a[index] = temp;
}
}
}

它的时间复杂度是O(n^2),因为它总是要循环那么多次,并且每次都是从待排序的数据中挑选最小的,因此它是不稳定的排序算法。

插入排序

点击看答案 插入排序的原理类似于打牌的时候抓牌,每次抓牌上来,都按照顺序将其插入到之前排好序的牌堆中。
1
2
3
4
5
6
7
8
9
10
11
public void doInsertSort(){
for(int index = 1; index<length; index++){//外层向右的index,即作为比较对象的数据的index
int temp = array[index];//用作比较的数据
int leftindex = index-1;
while(leftindex>=0 && array[leftindex]>temp){//当比到最左边或者遇到比temp小的数据时,结束循环
array[leftindex+1] = array[leftindex];
leftindex--;
}
array[leftindex+1] = temp;//把temp放到空位上
}
}

时间复杂度是O(n^2),适用于数据量较少的情况,是稳定的排序。

shell排序

点击看答案 原理: 严格来说是基于插入排序的思想,shell排序有点不大好理解,后续再看看
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 思路:三层循环
* 第一层循环:控制增量-增量随着程序的进行依次递减一半
* 第二层循环:遍历数组
* 第三层循环:比较元素,交换元素。
* 这里需要注意的是:比较的两个元素和交换的两个元素是不同的。
*/
public static shellSort(int[] data) {
for (int div = data.length/2; div>0; div/=2) {
for (int j = div; j < data.length; j++) {
int temp = data[j];
for (int k=j; k>=div && temp<data[k-div] ; k-=div) {
data[k] = data[k-div];
}
data[k] = temp;
}
}
}

shell排序最差的时间复杂度是 O(n^2),平均复杂度是 O(nlogn),是不稳定的排序

快速排序

点击看答案

快速排序的思想是分治思想。假设我们现在对“6 1 2 7 9 3 4 5 10 8”这10个数进行排序。首先在这个序列中随便找一个数作为基准数,为了方便,就让第一个数6作为基准数吧。接下来,需要将这个序列中所有比基准数大的数放在6的右边,比基准数小的数放在6的左边。得到一个一个以6为中心的序列,再以6为界限,将左右两边都看成数组,分别按照刚才的方法排序。上个图会比较直观:

快速排序

代码如下:

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
public static void quickSort(int[] arr,int low,int high){
int i,j,temp,t;
if(low>high){
return;
}
i=low;
j=high;
//temp就是基准位
temp = arr[low];

while (i<j) {
//先看右边,依次往左递减
while (temp<=arr[j]&&i<j) {
j--;
}
//再看左边,依次往右递增
while (temp>=arr[i]&&i<j) {
i++;
}
//如果满足条件则交换
if (i<j) {
t = arr[j];
arr[j] = arr[i];
arr[i] = t;
}

}
//最后将基准为与i和j相等位置的数字交换
arr[low] = arr[i];
arr[i] = temp;
//递归调用左半数组
quickSort(arr, low, j-1);
//递归调用右半数组
quickSort(arr, j+1, high);
}

快排的平均时间复杂度是 O(nlogn) ,最坏情况下为 O(n^2),这种交换方式导致它是不稳定的排序。

堆排序

点击看答案

堆排序是一种选择排序

堆排序时,先构建堆(假设大顶堆),将数组转换成堆,数据在堆中是按层编号的,所以数组中一个编号为 i 的结点的子结点在 2i + 1 和 2i + 2 的位置。开始构建时,首先从最后一个非叶子结点开始(叶子结点不用调,叶子结点只是非叶子结点比较时被动移动),最后一个非叶子节点的位置在 n/2-1。

构建了大顶堆后,堆顶元素与末尾元素交换,将大元素“沉”到末尾,将除尾部以外的元素再构建大顶堆,如此循环,每次找到最大的下沉的后面。

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
43
private static void heapSort(int[] arr){
int arrLen = arr.length;
int temp;

//建立大顶堆
for (int i = arrLen/2 -1;i>=0;i--){
//从第一个非叶子结点(在 arrLen/2 -1 处)从下至上,从右至左调整结构
adjustHeap(arr,i,arrLen);

}

for (int j = arrLen -1;j >= 0;j--){
//将堆顶元素与末尾元素进行交换
temp = arr[0];
arr[0] = arr[j];
arr[j] = temp;

//重新对堆进行调整
adjustHeap(arr,0,j);
}
}

private static void adjustHeap(int[] arr,int start,int end){
//先取出当前元素i
int temp = arr[start];

for (int k = 2*start + 1;k < end;k = 2*k + 1){//从i结点的左子结点(2i+1处)开始
if (k + 1 < end && arr[k] < arr[k + 1]){//如果左子结点小于右子结点,k指向右子结点
k ++;
}

//如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
if (arr[k] > temp) {
arr[start] = arr[k];
start = k;
}else {
break;
}
}

//将temp值放到最终的位置
arr[start] = temp;
}

它的最坏,最好以及平均复杂度都是 O(nlogn),它是不稳定排序。

以上内容参考自他人的博客

归并排序

点击看答案

归并排序是基于 分治法 实现的。目前还看不大懂,后续再理解

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
    //非递归算法实现二路归并排序,length代表数组长度,即数组最大下标是 legth - 1
void mergeSort(int List[],int length){
int size = 1;
int low;
int mid;
int high;
//size 是标记当前各个归并序列的high-low,从1,2,4,8,……,2*size
while(size <= length - 1){
//从第一个元素开始扫描,low代表第一个分割的序列的第一个元素
low = 0;
//当前的归并算法结束的条件
while(low + size <= length - 1){
//mid代表第一个分割的序列的最后一个元素
mid = low + size - 1;
//high 代表第二个分割的序列的最后一个元素
high = mid + size;
//判断一下:如果第二个序列个数不足size个
if(high > length - 1){
//调整 high 为最后一个元素的下标即可
high = length - 1;
}
//调用归并函数,进行分割的序列的分段排序
merge(List, low, mid, high);
//打印出每次归并的区间
cout << "low:" << low << " mid:" << mid << " high:" << high << endl;
//下一次归并时第一序列的第一个元素位置
low = high + 1;
}// end of while
//范围扩大一倍,二路归并的过程
size *= 2;
}
}

归并的思想主要用于外部排序:
外部排序可分两步
①待排序记录分批读入内存,用某种方法在内存排序,组成有序的子文件,再按某种策略存入外存。
②子文件多路归并,成为较长有序子文件,再记入外存,如此反复,直到整个待排序文件有序。

总结

上述排序,稳定的排序有:冒泡、插入、合并 ,不稳定排序:选择、shell、快排、堆排

二叉树遍历

先序遍历

先序遍历

点击看答案

递归方法:

1
2
3
4
5
6
7
8
9
10
11
void pre(BTreeNode treeNode){

if(treeNode != null){
//显示节点数据
showNodeValue(treeNode);
//先序遍历左子树
pre(treeNode.left);
//先序遍历右子树
pre(treeNode.right)
}
}

非递归方法:

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
//非递归,手算的思想,先变访问边找,找到最左下方的,然后向上再向访问右边的
public static void iterativePreOrder(TreeNode p) {
if (p == null) return;
Stack<TreeNode> stack = new Stack<TreeNode>();
while (!stack.empty() || p != null) {
while (p != null) {
visit(p);
stack.push(p);
p = p.left;
}
p = stack.pop();
p = p.right;
}
}

//栈的思想,按层次倒着进栈,利用后进先出解决顺序问题
public static void iterativePreOrder_2(TreeNode p) {
if (p == null) return;
Stack<TreeNode> stack = new Stack<TreeNode>();
stack.push(p);
while (!stack.empty()) {
p = stack.pop();
visit(p);
if (p.right != null) stack.push(p.right);
if (p.left != null) stack.push(p.left);
}
}

中序遍历

中序遍历

点击看答案

递归方法:

1
2
3
4
5
6
7
8
9
10
public static void middle(BTreeNode treeNode){
if(treeNode != null){
//中序遍历左子树
middle(treeNode.left);
//显示节点数据
showNodeValue(treeNode);
//中序遍历右子树
middle(treeNode.right);
}
}

非递归方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void iterativeInOrder(TreeNode p) {
if (p == null) return;
Stack<TreeNode> stack = new Stack<TreeNode>();
while (!stack.empty() || p != null) {
while (p != null) {
stack.push(p);
p = p.left;
}
p = stack.pop();
visit(p);
p = p.right;
}
}

后序遍历

后序遍历

点击看答案 递归方法:
1
2
3
4
5
6
7
8
9
10
11
public static void behind(BTreeNode treeNode){
if(treeNode != null){
//后序遍历左子树
behind(treeNode.left);
//后序遍历右子树
behind(treeNode.right);
//显示节点数据
showNodeValue(treeNode);

}
}

非递归方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//双栈法,易于理解
public static void iterativePostOrder_3(TreeNode p) {
if (p == null) return;
Stack<TreeNode> stack = new Stack<TreeNode>();
Stack<TreeNode> result = new Stack<TreeNode>();
while (!stack.empty() || p != null) {
while (p != null) {
stack.push(p);
result.push(p);
p = p.right;
}
if (!stack.empty()) p = stack.pop().left;
}
while (!result.empty()) {
p = result.pop();
visit(p);
}
}

层次遍历

点击看答案
1
2
3
4
5
6
7
8
9
10
11
public static void iterativeLevelOrder(TreeNode p) {
if (p == null) return;
LinkedList<TreeNode> queue = new LinkedList<TreeNode>();
queue.offer(p);
while (!queue.isEmpty()) {
p = queue.poll();
if (p.left != null) queue.offer(p.left);
if (p.right != null) queue.offer(p.right);
visit(p);
}
}

以上内容可以参考这篇博客

扑克洗牌算法

打乱一个已有顺序有多种实现方式,但是要高效地实现,还需要斟酌,以下是目前能想到的最有的解决方案:

1
2
3
4
5
6
7
8
9
10
11
//生成一副牌
Card[52] oneCard = generateOneCard;

//顺序与 随机位置交换
Random r = new Random();
for (int i = 0; i < oneCard.size(); i ++){
int j = r.nextInt(52);
Card tempCard = oneCard[i];
oneCard[i] = onCard[j];
onCard[j] = tempCard;
}

判断链表中是否有环

快慢指针法:创建两个指针1和2同时指向这个链表的头节点,然后两个指针分别向后移动,其中指针1每次向后移动一个节点,指针2每次向后移动两个节点,每移动一次就比较两个指针指向的节点是否相同,如果相同说明出链表有环;如果不同,则继续循环,直到有环结束或者到达尾部结束。

原理:两个人在环形跑道上同一位置开始跑,一人速度快,一人速度慢,如此持续跑一段时间,速度快的那个肯定会从速度慢的身后再次追上以及超越,这中间必然有个交汇点。如果是跑直线的话,到终点就结束了,不会再碰面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//判断是否有环
public static <T> boolean isLoopList(ListNode<T> head){
ListNode<T> slowPointer, fastPointer;

//使用快慢指针,慢指针每次向前一步,快指针每次两步
slowPointer = fastPointer = head;
while(fastPointer != null && fastPointer.next != null){
slowPointer = slowPointer.next;
fastPointer = fastPointer.next.next;

//两指针相遇则有环
if(slowPointer == fastPointer){
return true;
}
}
return false;
}

引申:如何判断环的入口

我们假定链表头到环入口的距离是len,环入口到slow和fast交汇点的距离为x,环的长度为R。slow和fast第一次交汇时,设slow走的长度为:d = len + x,而fast走的长度为:2d = len + nR + x,(n >= 1),从而我们可以得知:2len + 2x = len + nR + x,即len = nR - x,(n >= 1)。所以,要找出环入口,也要两个指针,一个指针A指向相遇时候的节点,一个指针B指向链表头,两个指针每次都走一步,A指针在遍历过程中可能多次(n >= 1)经过环入口节点,但当B指针第一次达到入口节点时,A指针此时必然也指向入口节点。

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
//判断是否有环,有环就返回入口
public static <T> ListNode<T> findEntranceInLoopList(ListNode<T> head){
ListNode<T> slowPointer, fastPointer;

//使用快慢指针,慢指针每次向前一步,快指针每次两步
boolean isLoop = false;
slowPointer = fastPointer = head;
while(fastPointer != null && fastPointer.next != null){
slowPointer = slowPointer.next;
fastPointer = fastPointer.next.next;

//两指针相遇则有环
if(slowPointer == fastPointer){
isLoop = true;
break;
}
}

//一个指针从链表头开始,一个从相遇点开始,每次一步,再次相遇的点即是入口节点
if(isLoop){
slowPointer = head;
while(fastPointer != null && fastPointer.next != null){
//两指针相遇的点即是入口节点
if(slowPointer == fastPointer){
return slowPointer;
}

slowPointer = slowPointer.next;
fastPointer = fastPointer.next;
}
}
return null;
}

再引申,两个单链表是否相交

两个没有环的链表在某一节点相交,那么在这个节点之后的所有结点都是两个链表所共有的。如果它们相交,则最后一个结点一定是共有的,因此问题转化为:两个链表最后一个结点是否相同(时间复杂度为O(len1+len2))。要找出相交的第一个结点,可以首先获得两个链表的长度,然后获得两个链表长度差值 K,之后长的链表指向第K个结点,短的链表从头开始,每次向后移动一个结点,再比较当前结点是否相等,第一次相等的那个结点点就是相交节点。

代码略

1、给100盏灯编号 1~100,开始时所有灯都是关着的
第1次:把所有编号是1的倍数的灯的开关状态改变一次;
第2次:把所有编号是2的倍数的灯的开关状态改变一次;
第3次,把所有编号是3的倍数的灯的开关状态改变一次;

第100次,把所有编号是100的倍数的灯的开关状态改变一次;
问:最后开着的灯的编号是哪些?

分析:最开始灯是灭的,因此只有经过奇数次开关状态改变,灯才会是亮的。从题意可知一个数字有多少约数就会状态改变多少次,因此这道题可以转换为:1~100数字中约数个数是奇数的有哪些。并且我们知道约数是成对出现的,如8的约数:(1,8)、(2,4),因此如果要出现约数的个数是奇数个,除非它是个平方数,如36的约数:(1,36)、(2,18)、(3,12)、(4,9)、(6),因此,这题又可以转换为1~100数字中的平方数有哪些,这操作。。。。666吧,这就是分析问题的乐趣吧

2、烧一根不均匀的绳子,从头烧到尾总共需要1小时,现在有若干条这样的绳子,问如何用烧绳子的方法来计时1小时15分钟呢

从题目知道,绳子不均匀,不能根据燃烧半根来计量半小时。但是还可以推断,从两头烧,只要半小时。因此可以使用3根绳子来计时:1)第1根绳子点燃两端,第2根绳子点燃一端,第三根绳子先不点燃; 2)第1根绳子烧完计时30分钟,接着第2根绳子另一端也点燃 3)第2根绳子烧完计时15分钟,此时已经计时45分钟了,剩下的半小时只需要将第3条绳子两端点燃就能计算出。

3、有12个外观一样的小球,但有一个与其他小球重量略微不同,用手感觉不出来,用一个天平,能称3次就能找到那个小球吗

把小球编号 112,然后分为3组(想想为什么是3组):14分为A组,58分为B组,912分为C组。首先A组和B组称量比较:
1、天平平衡。则目标小球在C组。将C分为两组:9、10、11分为一组C1,12分为一组C2,再从B中随意拿出3个球分组为B1,C1和B1比较:
1)若平衡,则12号球就是目标球。
2)若不平衡,则目标球在C1内,并且根据天平倾斜,可以判断目标球比普通球重还是轻(若是天平显示B1重,则目标球比较轻;反之目标球比较重),此时随意取 C1 中的2个球放在天平上即可知道结果(如果平衡,则目标球是另一个,如果不平衡,根据轻重可知目标球)。
2、天平不平衡。则目标球在A或者B中。
1)若A > B,取(1,2,3,5)为一组X,(4,9,10,11)为一组Y,(6,7,8)为一组Z,比较X和Y,若X>Y,可知不同小球在1,2,3中,且目标球重于普通球,此时再将编号 1,2,3 任取2个放上天平即可知道目标球。若X<Y,则说明4号是目标球;若X=Y,则目标球在Z中(6,7,8),并且目标球轻于普通球,此时只需要将编号 其中任意两个球放上天平即可找出目标球。
2)若 A < B,同样按照上述分为X、Y、Z三组,同理,若X<Y,则目标球在1,2,3中,且目标球轻于普通球,此时再将编号 1,2,3 任取2个放上天平即可知道目标球;若X>Y,说明4号为目标球,若X=Y,则目标球在Z中(6,7,8),并且目标球重于普通球,此时再将编号 6,7,8 任取2个放上天平即可知道目标球。

3、一笔画出经过9个点的4条直线

主要是要突破点,不要局限于点上,不说了,直接上图:
9点4线

9点4线

4、有1块钱1瓶的契税,喝完后2个空瓶换一瓶汽水,问有20块钱,可以喝几瓶汽水?

总共40瓶(不要浪费最后那个空瓶,找老板借1个,凑2瓶子再喝一瓶,最后这空瓶还给老板。。。)

5、时针分针秒针重合的次数

只有2次,一次是 00:00:00 一次是 12:00:00

假设排列100个球,两个人轮流一拿球装入口袋,能拿到第100个球为胜,条件是每次拿球至少拿1个,最多拿5个,加入你是最先拿球的人,你该拿几个,以后怎么保证拿到最后一个球?

怎样保证拿到最后一个球呢?最简单的方法是最后剩下6个,并且轮到对方来拿,这样无论对方拿几个,都能保证自己拿到最后一个。
(1)首先,要控制每一轮拿出的个数,但是对方拿的个数是不受控制的,假设对方拿n个,自己就拿6-n个(为什么是控制总数是6个,是因为对手最多拿5个,自己最少能拿1个,和值就是6,自己不能把和值控制得更低;而对手至少拿1个,自己最多拿5个,同样自己不能把和值控制得更高。因此只有6才是一个可控的值)。
(2)其次,假设第一次拿x个球,以后每次自己和对方拿的和是6,最多可以拿到15轮(除去第一轮的x个,最后留这肯定少于10个了),还剩 10-x个,为了达到之前定的目标最后留6个,所以x应该是4.
(3)总结而言就是第一次拿4个,以后每次对方拿了n个后,自己拿 6-n 个。

如何确定用户异地登陆。

Android 事件分发机制

当我们点击屏幕时,事件最先传递给Activity ,在 Activity 的dispatchTouchEvent() 回调中,默认首先调用 getWindow().superDispatchTouchEvent(ev) 将事件交给window 处理,如果window 返回true之后,则直接return true。在 getWindow().superDispatchTouchEvent(ev) 调用的时候,我们能发现事件一步步传递:

1、PhoneWindow.superDispatchTouchEvent
2、DecorView.dispatchTouchEvent()
3、ViewGroup. dispatchTouchEvent()

代码可以参考(Carson的):

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/**  * 源码分析:Activity.dispatchTouchEvent()  */ 
public boolean dispatchTouchEvent(MotionEvent ev) {

// 仅贴出核心代码

// ->>分析1
if (getWindow().superDispatchTouchEvent(ev)) {

return true;
// 若getWindow().superDispatchTouchEvent(ev)的返回true
// 则Activity.dispatchTouchEvent()就返回true,则方法结束。即 :该点击事件停止往下传递 & 事件传递过程结束
// 否则:继续往下调用Activity.onTouchEvent

}
// ->>分析3
return onTouchEvent(ev);
}

/** * 分析1:getWindow().superDispatchTouchEvent(ev) * 说明: * a. getWindow() = 获取Window类的对象 * b. Window类是抽象类,其唯一实现类 = PhoneWindow类 * c. Window类的superDispatchTouchEvent() = 1个抽象方法,由子类PhoneWindow类实现 */
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {

return mDecor.superDispatchTouchEvent(event);
// mDecor = 顶层View(DecorView)的实例对象
// ->> 分析2
}

/** * 分析2:mDecor.superDispatchTouchEvent(event) * 定义:属于顶层View(DecorView) * 说明: * a. DecorView类是PhoneWindow类的一个内部类 * b. DecorView继承自FrameLayout,是所有界面的父类 * c. FrameLayout是ViewGroup的子类,故DecorView的间接父类 = ViewGroup */
public boolean superDispatchTouchEvent(MotionEvent event) {

return super.dispatchTouchEvent(event);
// 调用父类的方法 = ViewGroup的dispatchTouchEvent()
// 即将事件传递到ViewGroup去处理,详细请看后续章节分析的ViewGroup的事件分发机制

}
// 回到最初的分析2入口处

/** * 分析3:Activity.onTouchEvent() * 调用场景:当一个点击事件未被Activity下任何一个View接收/处理时,就会调用该方法 */
public boolean onTouchEvent(MotionEvent event) {

// ->> 分析5
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}

return false;
// 即 只有在点击事件在Window边界外才会返回true,一般情况都返回false,分析完毕
}

/** * 分析4:mWindow.shouldCloseOnTouch(this, event) * 作用:主要是对于处理边界外点击事件的判断:是否是DOWN事件,event的坐标是否在边界内等 */
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {

if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
&& isOutOfBounds(context, event) && peekDecorView() != null) {

// 返回true:说明事件在边界外,即 消费事件
return true;
}

// 返回false:在边界内,即未消费(默认)
return false;
}

之后,就进入了ViewGroup的事件分发流程。用一张图(这张图转载自Carson_Ho)来描绘下整体的过程:

Activity事件分发

接下来主要关注的是 Android 中 ViewGroup 事件分发机制。它总体来说可以用几张图说明白,相比在代码中打印Log,这种方式更容易理解和记住。

针对 ACTION_DOWN 事件

只针对 ACTION_DOWN 事件时,事件的完整流向是一个U形图,如下图所示:

只考虑ACTION_DOWN 事件时事件流向图

如果没有中断事件,可能看起来更直观一些,Activity、ViewGroup、View的层次更加清晰:

不中断事件

针对 ACTION_MOVE 和 ACTION_UP

上面讲解的都是针对ACTION_DOWN的事件传递,ACTION_MOVE和ACTION_UP在传递的过程中并不是和ACTION_DOWN 一样,你在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个事件(如ACTION_DOWN)返回true,才会收到ACTION_MOVE和ACTION_UP的事件。

ViewGroup1 的dispatchTouchEvent 中返回true消费事件

红色的箭头代表ACTION_DOWN 事件的流向,蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向:

viewgroup1消费

ViewGroup2 的dispatchTouchEvent 中返回true消费事件

viewgroup2消费

在View 的dispatchTouchEvent 返回true消费事件,就不画图了,效果和在ViewGroup2 的dispatchTouchEvent return true的差不多,同样的收到ACTION_DOWN 的dispatchTouchEvent函数都能收到 ACTION_MOVE和ACTION_UP。

在View 的onTouchEvent 中返回true消费事件

在View 的onTouchEvent 中返回true消费事件

在ViewGroup 2 的onTouchEvent 中返回true消费事件

在ViewGroup 2 的onTouchEvent 中返回true消费事件

在ViewGroup 1 的onTouchEvent 中返回true消费事件

在ViewGroup 1 的onTouchEvent 返回true消费这次事件

在View的dispatchTouchEvent 中返回false并且ViewGroup 1 的onTouchEvent 返回true消费事件

在View的dispatchTouchEvent 返回false并且Activity 的onTouchEvent 返回true消费这次事件

在View的dispatchTouchEvent 中返回false并且Activity 的onTouchEvent 返回true消费事件

在View的dispatchTouchEvent 返回false并且Activity 的onTouchEvent 返回true消费这次事件

经过这么多图能得出以下规律:

ACTION_DOWN事件在哪个控件消费了(return true), 那么ACTION_MOVE和ACTION_UP就会从上往下(通过dispatchTouchEvent)做事件分发往下传,就只会传到这个控件,不会继续往下传,如果ACTION_DOWN事件是在dispatchTouchEvent消费,那么事件到此为止停止传递,如果ACTION_DOWN事件是在onTouchEvent消费的,那么会把ACTION_MOVE或ACTION_UP事件传给该控件的onTouchEvent处理并结束传递。

以上文章参考自 Kelin ,这里图片形式贴出来仅仅只是个人做的笔记,方便记忆。