美文网首页
Android面试题总结

Android面试题总结

作者: 寄浮生 | 来源:发表于2020-08-10 09:04 被阅读0次

    [TOC]

    1 JAVA:

    String为什么这么设计

    在源码中string是用final 进行修饰,它是不可更改,不可继承的常量。

    1、字符串池的需求

    字符串池是方法区(Method Area)中的一块特殊的存储区域。当一个字符串已经被创建并且该字符串在 池 中,该字符串的引用会立即返回给变量,而不是重新创建一个字符串再将引用返回给变量。如果字符串不是不可变的,那么改变一个引用(如: string2)的字符串将会导致另一个引用(如: string1)出现脏数据。

    2、允许字符串缓存哈希码

    在java中常常会用到字符串的哈希码,例如: HashMap 。String的不变性保证哈希码始终唯一,因此,他可以不用担心变化的出现。 这种方法意味着不必每次使用时都重新计算一次哈希码——这样,效率会高很多。

    3、安全

    String广泛的用于java 类中的参数,如:网络连接(Network connetion),打开文件(opening files )等等。如果String不是不可变的,网络连接、文件将会被改变——这将会导致一系列的安全威胁。操作的方法本以为连接上了一台机器,但实际上却不是。由于反射中的参数都是字符串,同样,也会引起一系列的安全问题。

    Object类的equal和hashCode方法重写,为什么?

    首先equals与hashcode间的关系是这样的:

    1、如果两个对象相同(即用equals比较返回true),那么它们的hashCode值一定要相同;

    2、如果两个对象的hashCode相同,它们并不一定相同(即用equals比较返回false)

    由于为了提高程序的效率才实现了hashcode方法,先进行hashcode的比较,如果不同,那没就不必在进行equals的比较了,这样就大大减少了equals比较的次数,这对比需要比较的数量很大的效率提高是很明显的

    Serializable 和Parcelable 的区别

    Serializable Java 序列化接口 在硬盘上读写 读写过程中有大量临时变量的生成,内部执行大量的i/o操作,效率很低。

    Parcelable Android 序列化接口 效率高 使用麻烦 在内存中读写(AS有相关插件 一键生成所需方法) ,对象不能保存到磁盘中

    为什么要序列化?

    Android中不同进程通信(传输数据)需要序列化

    序列化:对象转二进制数据

    反序列化:二进制数据转对象

    序列化用途:网络上传输数据,跨进程

    静态代理和动态代理的区别,什么场景使用?

    静态代理与动态代理的区别在于代理类生成的时间不同,即根据程序运行前代理
    类是否已经存在,可以将代理分为静态代理和动态代理。如果需要对多个类进行
    代理,并且代理的功能都是一样的,用静态代理重复编写代理类就非常的麻烦,
    可以用动态代理动态的生成代理类。

    静态代理使用场景:四大组件同 AIDL 与 AMS 进行跨进程通信
    动态代理使用场景:Retrofit 使用了动态代理极大地提升了扩展性和可维
    护性。

    java虚拟机,Dalvik虚拟机和Art虚拟机的区别

    • Java虚拟机:

    1、java虚拟机基于栈。 基于栈的机器必须使用指令来载入和操作栈上数据,所需指令更多更多。

    2、java虚拟机运行的是java字节码。(java类会被编译成一个或多个字节码.class文件)

    • Dalvik虚拟机:

    1、dalvik虚拟机是基于寄存器的

    2、Dalvik运行的是自定义的.dex字节码格式。(java类被编译成.class文件后,会通过一个dx工具将所有的.class文件转换成一个.dex文件,然后dalvik虚拟机会从其中读取指令和数据

    3、常量池已被修改为只使用32位的索引,以 简化解释器。

    4、一个应用,一个虚拟机实例,一个进程(所有android应用的线程都是对应一个linux线程,都运行在自己的沙盒中,不同的应用在不同的进程中运行。每个android dalvik应用程序都被赋予了一个独立的linux PID(app_*))

    • Art虚拟机:

    即Android Runtime,Android 4.4发布了一个ART运行时,准备用来替换掉之前一直使用的Dalvik虚拟机。(5.0以后默认开启)

    ART 的机制与 Dalvik 不同。在Dalvik下,应用每次运行的时候,字节码都需要通过即时编译器(just in time ,JIT)转换为机器码,这会拖慢应用的运行效率,而在ART 环境中,应用在第一次安装的时候,字节码就会预先编译成机器码,使其成为真正的本地应用。这个过程叫做预编译(AOT,Ahead-Of-Time)。这样的话,应用的启动(首次)和执行都会变得更加快速。

    Dalvik与Art的区别:

    1. Dalvik每次都要编译再运行,Art只会首次启动编译
    2. Art占用空间比Dalvik大(原生代码占用的存储空间更大),就是用“空间换时间”
    3. Art减少编译,减少了CPU使用频率,使用明显改善电池续航
    4. Art应用启动更快、运行更快、体验更流畅、触感反馈更及时

    1.1 进程,线程:

    开启线程的方式

    新启线程的方式只有2种

    1:Thread

    2:Runnable

    //Thread 源码73行 注释只有2种,

    thread没有接收Callable的实现

    //Callable 包装成了Future,Future实现RunnableFuture接口,RunnableFuture继承自Runnable

    run()和start()方法区别

    start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。

    sleep、wait、yield的区分,wait的线程如何唤醒它?

    sleep让当前线程休眠,wait等待,yield是让出当前线程执行权,不释放锁 yield 运行->就绪。

    sleep,yield不会释放锁, wait会释放,被唤醒时重新去争夺锁。 唤醒:notify/notifyall

    slep,yield是暂停当前线程,继续持有 wait用来线程之间切换资源

    线程池的基本原理

    core -> queue -> max->拒绝策略

    先核心线程数(corePool)再阻塞队列(BlockingQueue)再最大线程数(maximumPool)最后RejectedExecutionHandler(4种

    尽量使用有界队列

    为什么使用线程池:

    1.复用线程,降低创建,销毁资源消耗。

    2.提高响应速度(线程:T1创建时间+T2执行时间+T3销毁时间,线程池:T2)。

    3.提高线程可管理性。

    AsyncTask

    线程数=CPU_COUNT+1:+1为了防止产生页缺失的时候,我还有线程可以用。(为了让cpu不空闲)

    SerialExecutor为了使任务串行执行,

    ThreadPoolExecutor真正执行任务。

    ThreadPoolExecutor为静态,实例多个AsyncTask也是使用同一个线程池。

    sychronied修饰普通方法和静态方法的区别

    普通方法是对象的实例(new xxx())

    静态是锁的 xx.class对象 xx.class在虚拟机中是唯一的,独一份

    Synchronized的原理以及与ReentrantLock的区别

    使用monitorenter和monitorexit指令来实现

    sychonize.png

    一个显示锁一个内置锁,一个是关键字一个是对象。ReentrantLock花式取锁,并且实现了公平与非公平锁

    Synchronized做了哪些优化

    1.6以后

    偏向锁->轻量级锁(适应性轻量级锁)->重量级锁

    锁消除,锁粗化(将两个代码块合二为一(包在锁内),减少上下文切换),逃逸分析

    ThreadLocal是什么?

    Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。所以ThreadLocal的应用场合,最适合的是按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。

    数据隔离的秘诀其实是这样的,Thread有个TheadLocalMap类型的属性,叫做threadLocals,该属性用来保存该线程本地变量。这样每个线程都有自己的数据,就做到了不同线程间数据的隔离,保证了数据安全。

    CAS无锁编程的原理

    使用了现代cpu为我们提供的cas指令。

    CAS 地址上的变量,变量值和期望一样,交换为新值 (比较并且交换)不一样,就自旋

    CAS三大问题:1ABA。2开销 3只适用简单变量

    可见性

    一个线程修改了变量以后,其他线程能够立即看到修改的值 (解决可见性问题volatile,加锁)

    锁分为哪几类

    lock-all.png

    如何规避ABA

    加版本号

    AtomicInteger#incrementAndGet() 实现

    锁升级步骤

    lock.png

    偏向锁:记录线程id,下次调用如果id一样就直接用,如果id不一样就要过度到轻量锁(自旋锁):ps:偏向锁启动有个延迟,4秒以后打开

    偏向锁为什么要延迟4秒(打开偏向锁是否效率一定会提升?)

    明知道有多线程要竞争时,就没必要开启偏向锁。

    jvm启动过程中,一定会产生多线程竞争,所以延迟了4秒再打开偏向锁

    设置偏向锁:jvm调优

    -XX:BiasedLockingStartupDelay=0 默认4,设成0直接匿名偏向

    轻量级压力大,就升级到重量锁(自旋一定次数后,扔到了wait队列,排队 ,1.6前自旋10次,自旋等待数为cpu二分之一,1.6以后自适应自旋)

    sychronized是否支持可重入?为什么要支持,怎么样支持

    支持,因为继承(子类里重写方法,上锁),上一次锁在线程栈里生成一个LR,解锁一个弹出一个LR(LR会记录锁的级别,偏向,轻量,重量)

    DCL单例需不需要加volatile?

    需要,防止指令重排序(CPU乱序执行)

    线程的状态/线程的生命周期

    thread.png

    1.2 JVM相关:

    整体:JVM运行过程

    jvm-all.png

    类加载过程:加载,校验,准备,解析,初始化,使用,卸载
    加载
    加载一个Class需要完成以下3件事:

    • 通过Class的全限定名获取Class的二进制字节流
    • 将Class的二进制内容加载到虚拟机的方法区
    • 在内存中生成一个java.lang.Class对象表示这个Class

    获取Class的二进制字节流这个步骤有多种方式:

    • 从zip中读取,如:从jar、war、ear等格式的文件中读取Class文件内容
    • 从网络中获取,如:Applet
    • 动态生成,如:动态代理、ASM框架等都是基于此方式
    • 由其他文件生成,典型的是从jsp文件生成相应的Class

    校验
    验证一个Class的二进制内容是否合法,主要包括4个阶段:

    • 文件格式验证,确保文件格式符合Class文件格式的规范。如:验证魔数、版本号等。
    • 元数据验证,确保Class的语义描述符合Java的Class规范。如:该Class是否有父类、是否错误继承了final类、是否一个合法的抽象类等。
    • 字节码验证,通过分析数据流和控制流,确保程序语义符合逻辑。如:验证类型转换是合法的。
    • 符号引用验证,发生于符号引用转换为直接引用的时候(转换发生在解析阶段)。如:验证引用的类、成员变量、方法的是否可以被访问(IllegalAccessError),当前类是否存在相应的方法、成员等(NoSuchMethodError、NoSuchFieldError)。

    准备

    在准备阶段,虚拟机会在方法区中为Class分配内存,并设置static成员变量的初始值为默认值。注意这里仅仅会为static变量分配内存(static变量在方法区中),并且初始化static变量的值为其所属类型的默认值。如:int类型初始化为0,引用类型初始化为null。即使声明了这样一个static变量:

    public static int a = 123;
    

    在准备阶段后,a在内存中的值仍然是0, 赋值123这个操作会在中初始化阶段执行,因此在初始化阶段产生了对应的Class对象之后a的值才是123 。

    解析

    解析阶段,虚拟机会将常量池中的符号引用替换为直接引用,解析主要针对的是类、接口、方法、成员变量等符号引用。在转换成直接引用后,会触发校验阶段的符号引用验证,验证转换之后的直接引用是否能找到对应的类、方法、成员变量等。这里也可见类加载的各个阶段在实际过程中,可能是交错执行。

    初始化

    初始化阶段即开始在内存中构造一个Class对象来表示该类,即执行类构造器<clinit>()的过程。需要注意下,<clinit>()不等同于创建类实例的构造方法<init>()

    • <clinit>()方法中执行的是对static变量进行赋值的操作,以及static语句块中的操作。
    • 虚拟机会确保先执行父类的<clinit>()方法。
    • 如果一个类中没有static的语句块,也没有对static变量的赋值操作,那么虚拟机不会为这个类生成<clinit>()方法。
    • 虚拟机会保证<clinit>()方法的执行过程是线程安全的。
      因此,存在如下一种最简单的单例模式的实现:

    运行时数据区

    run-data.png

    虚拟机栈

    jvm-stack.png

    Java中堆和栈有什么不同?

    栈是一块和线程紧密相关的内存区域。每个线程都有自己的栈内存,用于存储本地变量,方法参数和栈调用,一个线程中存储的变量对其它线程是不可见的。而堆是所有线程共享的一片公用内存区域。对象都在堆里创建,为了提升效率线程会从堆中弄一个缓存到自己的栈,如果多个线程使用该变量就可能引发问题,这时volatile 变量就可以发挥作用了,它要求线程从主存中读取变量的值。

    对象的内存布局

    object.png

    对象的分配策略

    object-fenpei.png

    几乎所有的对象都在堆上分配

    栈上分配 :逃逸分析

    新生代老年代的空间比例

    新生代:老年代=1:2(默认)

    新生代

    Eden:From:To=8:1:1(默认)

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

    From,To区进行复制回收算法, GC计数 15次以后进入老年代

    survivor 幸存者区域,直接进入老年代

    一般来说Eden中90%对象会被回收掉,10%才进入From,To区。

    所以:Eden:From:To 默认是8:1:1

    强软弱虚引用:

    强引用:=

    软引用:马上OOM的时候回收 。(图片缓存)

    弱引用:一旦发生GC时就会被回收 (空间不够,才会GC)ThreadLocal,HashMap

    虚引用:监控垃圾回收器是否执行

    垃圾回收算法

    复制算法,标记清除,标记整理

    gc.png

    复制算法:(主要是新生代)

    实现简单,运行高效

    内存复制,没有内存碎片

    复用率只有一半

    标记-清除算法:

    执行效率不稳定

    内存碎片导致提前GC

    标记-整理算法(效率偏低):

    对象移动

    引用更新

    用户线程暂停

    没有内存碎片

    常用的垃圾收集器

    gc-tool.png

    CMS(Concurrent Mark Sweep)回收器

    (只针对老年代)标记清除算法

    cms.png

    1.初始标记 :根直接相连的对象 (暂停所有用户线程 )

    2并发标记:标记除根直接相连的 其他对象(并发标记)

    3重新标记:有变动的对象(暂停所有用户线程)

    4并发清理:用户和GC同时进行 时间长

    5重置线程:

    缺点:

    CPU敏感(核数<4基本上没法用)

    浮动垃圾(在并发清理时,还有新的垃圾产生)

    内存碎片

    G1回收器

    g1.png

    S:young,old H:大对象>512KB

    复制和标记整理

    jvm最大暂停时间 1000ms, 筛选回收

    可预测停顿(尽量往最小暂停时间上靠)

    CMS和G1的平衡点

    6~8G 堆空间 G1效率比CMS效率高一点

    Stop The World

    stw.png

    危害:卡顿

    常量池与String

    string.png
    • 静态常量池:class

      存放内容:

      字面量 :String i="king" king就字面量

      符号引用: String 这个类,java.lang.String

      其他的:代码,类,方法的信息

    • 运行时常量池

      类加载--运行时数据区--方法区(逻辑区域)

    String的创建分配内存地址

    final class String :不可以被继承

    private final char value[];

    String只要创建了,就不可变

    String str="abc";

    String str2="abc"; 就不会再创建了,

    String str1=new String("abc"); //编译时,就在堆里了

    会有2个对象,一个字符串常量池里"abc",一个是堆里的String对象(abc的引用)

    字符串拼接

    String str = "ab"+"cd"+"ef";

    首先会生成ab对象,再生成abcd对象,最后生成abcdef对象。所以尽量不做字符串拼接,而是使用StringBuffer进行拼接。

    intern()

    intern()首先在常量池中查找是否有等于该字符串的对象引用,有就返回引用。

    所以a和b指向的是同一个

    intern.png

    -----------------------------

    什么情况下内存栈溢出?

    递归,导致栈溢出,(另一种就是创建。。创建。。线程。。导致OOM)

    new一个对象的过程?

    https://www.cnblogs.com/gjmhome/p/11401397.html

    https://zhuanlan.zhihu.com/p/104454201

    对象会不会分配在栈中?

    会,如果符合逃逸分析,会在栈上分配。

    判断一个对象是否被回收,有哪些算法

    引用计数法

    可达性分析法

    哪些是GCRoot

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

    垃圾回收算法,特点?

    复制,标记-清除,标记-整理

    JVM完整的GC流程

    老年代对象的分配和回收

    (1)老年代的对象一般来自于新生代中的长期存活对象。这里有一概念叫做年龄阈值,每个对象定义了年龄计数器,经过一次 Minor GC (在交换区)后年龄加1,对象年龄达到15次后将会晋升到老年代,老年代空间不够时进行 Full GC。当然这个参数仍是可以通过 JVM 参数(-XX:MaxTenuringThreshold,默认15)来调整。

    (2)大对象直接进入老年代。即超过 Eden 区空间,或超过一个参数值(-XX:PretenureSizeThreshold=30m,无默认值)。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。

    (3)对象提前晋升到老年代(组团)。动态年龄判定:如果在 Survivor 区中相同年龄所有对象大小总和大于 Survivor 区大小的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,而无须等到自己的晋升年龄。

    对象的正常流程:Eden 区 -> Survivor 区 -> 老年代。

    新生代GC:Minor GC;老年代GC:Full GC,比 Minor GC 慢10倍,JVM 会“stop the world”,严重影响性能。

    【总结】:内存区域不够用了,就会引发GC。Minor GC 避免不了,Full GC 尽量避免。

    【处理方式】:保存堆栈快照日志、分析内存泄漏、调整内存设置控制垃圾回收频率,选择合适的垃圾回收器等。

    final,finally,finalize的区别?

    PS:final:修饰

    类(不能继承),1、锁定。2、效率

    变量(成员变量,局部变量):常量,赋值一次,不能改。

    finally: try catch finally:最终一定会执行。

    finalize()方法 :垃圾回收时,拯救一次。

    String s=neww String("xxx"); 创建了几个对象?

    2个,在常量池中创建了“xxx”常量,在堆中创建了String对象,引用常量池中的"xxx",

    返回"xxx"的引用 。

    -----------------------------

    2 数据结构:

    List,Set,Map的区别

    Set是最简单的一种集合。集合中的对象不按特定的方式排序,并且没有重复对象。

    Set接口主要实现了两个实现类:

    HashSet: HashSet类按照哈希算法来存取集合中的对象,存取速度比较快

    TreeSet :TreeSet类实现了SortedSet接口,能够对集合中的对象进行排序。

    List的特征是其元素以线性方式存储,集合中可以存放重复对象。

    ArrayList() : 代表长度可以改变得数组。可以对元素进行随机的访问,向ArrayList()中插入与删除元素的速度慢。

    LinkedList(): 在实现中采用链表数据结构。插入和删除速度快,访问速度慢。

    Map 是一种把键对象和值对象映射的集合,它的每一个元素都包含一对键对象和值对象。 Map没有继承于Collection接口 从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。

    HashMap:Map基于散列表的实现。插入和查询“键值对”的开销是固定的。可以通过构造器设置容量capacity和负载因子load factor,以调整容器的性能。

    LinkedHashMap: 类似于HashMap,但是迭代遍历它时,取得“键值对”的顺序是其插入次序,或者是最近最少使用(LRU)的次序。只比HashMap慢一点。而在迭代访问时反而更快,因为它使用链表维护内部次序。

    TreeMap : 基于红黑树数据结构的实现。查看“键”或“键值对”时,它们会被排序(次序由Comparabel或Comparator决定)。TreeMap的特点在 于,你得到的结果是经过排序的。TreeMap是唯一的带有subMap()方法的Map,它可以返回一个子树。

    WeakHashMap :弱键(weak key)Map,Map中使用的对象也被允许释放: 这是为解决特殊问题设计的。如果没有map之外的引用指向某个“键”,则此“键”可以被垃圾收集器回收。

    2.1 HashMap:

    HashMap如何保证元素均匀分布

    hash & (length-1)

    通过Key值的hashCode值和hashMap长度-1做与运算
    hashmap中的元素,默认情况下,数组大小为16,也就是2的4次方,如果要自定义HashMap初始化数组长度,也要设置为2的n次方大小,因为这样效率最高。因为当数组长度为2的n次幂的时候,不同的key算出的index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了

    ArrayList跟HashMap是否线程安全,如何保证线程安全

    不是线程安全

    解决方法:

    ArrayList:1.继承ArrayList,然后重写它的方法,用synchronized关键字修饰

    1. 使用Collections.synchronizedList()

    HashMap:1.继承HashMap,并且使用synchronized关键字

    1. 使用Collections.sychronizedMap();
    2. 使用ConcurrentHas上Map替换。并不推荐新代码使用Hashtable,HashTable继承于Dictionary,任意时间只有一个线程能写Hashtable,并发性能不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。不需要线程安全的场景使用HashMap,需要线程安全的场合使用ConcurrentHashMap替换。

    ArrayMap优势与缺陷

    从以下几个角度总结一下:

    • 数据结构
      • ArrayMap和SparseArray采用的都是两个数组,Android专门针对内存优化而设计的
      • HashMap采用的是数据+链表+红黑树
    • 内存优化
      • ArrayMap比HashMap更节省内存,综合性能方面在数据量不大的情况下,推荐使用ArrayMap;
      • Hash需要创建一个额外对象来保存每一个放入map的entry,且容量的利用率比ArrayMap低,整体更消耗内存
      • SparseArray比ArrayMap节省1/3的内存,但SparseArray只能用于key为int类型的Map,所以int类型的Map数据推荐使用SparseArray;
    • 性能方面:
      • ArrayMap查找时间复杂度O(logN);ArrayMap增加、删除操作需要移动成员,速度相比较慢,对于个数小于1000的情况下,性能基本没有明显差异
      • HashMap查找、修改的时间复杂度为O(1);
      • SparseArray适合频繁删除和插入来回执行的场景,性能比较好
    • 缓存机制
      • ArrayMap针对容量为4和8的对象进行缓存,可避免频繁创建对象而分配内存与GC操作,这两个缓存池大小的上限为10个,防止缓存池无限增大;
      • HashMap没有缓存机制
      • SparseArray有延迟回收机制,提供删除效率,同时减少数组成员来回拷贝的次数
    • 扩容机制
      • ArrayMap是在容量满的时机触发容量扩大至原来的1.5倍,在容量不足1/3时触发内存收缩至原来的0.5倍,更节省的内存扩容机制
      • HashMap是在容量的0.75倍时触发容量扩大至原来的2倍,且没有内存收缩机制。HashMap扩容过程有hash重建,相对耗时。所以能大致知道数据量,可指定创建指定容量的对象,能减少性能浪费。
    • 并发问题
      • ArrayMap是非线程安全的类,大量方法中通过对mSize判断是否发生并发,来决定抛出异常。但没有覆盖到所有并发场景,比如大小没有改变而成员内容改变的情况就没有覆盖
      • HashMap是在每次增加、删除、清空操作的过程将modCount加1,在关键方法内进入时记录当前mCount,执行完核心逻辑后,再检测mCount是否被其他线程修改,来决定抛出异常。这一点的处理比ArrayMap更有全面。

    3 设计模式:

    设计模式六大原则:

    • 单一职责原则:就一个类来说,应该只有一个引起它变化的原因

    一个类做一件事情,避免职责过多。比如这种情况是不太好的,在一个Activity中既有bean文件,又有http请求,还有adapter等等,这就导致我们需要修改任何一个东西的时候都会导致Activity的改变,这样一来就有多个引起它变化的原因,不符合单一职责原则

    • 开放封闭原则:对扩展开放,对修改关闭

    对于扩展是开放的,对于修改是封闭的。尽量做到面对需求的改变时,我们的代码能保持相对稳定,通过扩展的方式应对变化,而不是修改原有代码实现

    • 里氏替换原则:子类可以扩展父类的功能,但不要改变父类原有的功能

    里氏替换原则是实现开放封闭原则的重要方式之一,我们知道,使用基类的地方都可以使用子类去实现,因为子类拥有基类的所有方法,所以在程序设计中尽量使用基类类型对对象进行定义,在运行时确定子类类型。

    • 依赖倒置原则:高层模块不应该依赖于底层模块,两者都应该依赖于抽象,抽象不应该依赖于细节,细节应该依赖于抽象

    依赖倒置原则针对的是模块之间的依赖关系,高层模块指调用端,底层模块指具体的实现类,抽象指接口或抽象类,细节就是实现类。该原则的具体表现就是模块间的依赖通过抽象发生,直线类之间不发生直接依赖关系,依赖通过接口或抽象类产生,降低耦合,比如MVP模式下,View层和P层通过接口产生依赖关系

    • 迪米特原则(最少知识原则):一个软件实体应该尽可能少的与其他实体发生相互作用

    迪米特原则要求我们在设计系统时,尽量减少对象之间的交互

    • 接口隔离原则:一个类对另一个类的依赖应该建立在最小的接口上

    接口隔离原则的关键是接口以及这个接口要小,如何小呢,也就是我们要为专门的类创建专门的接口,这个接口只对它有效,不要试图让一个接口包罗万象,要建立最小的依赖关系

    设计模式分类:

    创建型模式:静态工厂模式、工厂方法模式、抽象工厂模式、单例模式、建造者模式

    结构型模式:桥接模式、适配器模式、装饰器模式、代理模式、组合模式、外观模式

    行为型模式:模板方法模式、策略模式、观察者模式、责任链模式、命令模式、访问者模式

    4 Android:

    4.1 Activity,Fragment,View:

    APP启动过程:

    App启动时,AMS会检查这个应用程序所需要的进程是否存在,不存在就会请求Zygote进程启动需要的应用程序进程,Zygote进程接收到AMS请求并通过fock自身创建应用程序进程,这样应用程序进程就会获取虚拟机的实例,还会创建Binder线程池(ProcessState.startThreadPool())和消息循环(ActivityThread looper.loop),然后App进程,通过Binder IPC向sytem_server进程发起attachApplication请求;system_server进程在收到请求后,进行一系列准备工作后,再通过Binder IPC向App进程发送scheduleLaunchActivity请求;App进程的binder线程(ApplicationThread)在收到请求后,通过handler向主线程发送LAUNCH_ACTIVITY消息;主线程在收到Message后,通过反射机制创建目标Activity,并回调Activity.onCreate()等方法。到此,App便正式启动,开始进入Activity生命周期,执行完onCreate/onStart/onResume方法,UI渲染结束后便可以看到App的主界面。

    Context, Activity,Appliction 有什么区别?

    首先Activity和Application都是Context的子类。Context从字面上理解就是上下文的意思,在实际应用中它也确实是起到了管理上下文环境中各个参数和变量的总用,方便我们可以简单的访问到各种资源。虽然Activity和Application都是Context的子类,但是他们维护的生命周期不一样。前者维护一个Acitivity的生命周期,所以其对应的Context也只能访问该activity内的各种资源。后者则是维护一个Application的证明周期。

    Activity 的启动过程?

    • 点击App图标后通过 startActivity 远程调用到 AMS 中,AMS 中将新启动的 activity 以 activityrecord 的结构压入activity栈中,并通过远程 binder 回调到原进程,使得原进程进入pause状态,原进程pause 后通知 AMS 我 pause 了
    • 此时 AMS 再根据栈中Activity 的启动intent 中的 flag 是否含
      有 new_task 的标签判断是否需要启动新进程,启动新进程通过
      startProcessXXX 的函数
    • 启动新进程后通过反射调用 ActivityThread 的 的 main 函数, main
      函数中调用 looper.prepar 和 和 lopper.loop 启动消息队列循环机
      制。最后远程告知 AMS 我启动了。 AMS 回调 handleLauncherActivity 加载 activity 。在 handlerLauncherActivity 中会通过反射调用 Application 的 onCreate 和 activity 的 的 onCreate 以及通过 handleResumeActivity 中反射调用 Activity 的 的 onResume
    activity_start.png

    说下MeasureSpec这个类

    • 作用:通过宽测量值widthMeasureSpec 和高测量值heightMeasureSpec 决定 View 的大小

    • 组成:一个 32 位 int 值,高 2 位代表 SpecMode(测量模式),低 30 位代表 SpecSize( 某种测量模式下的规格大小)。

    • 三种模式:

      • UNSPECIFIED:父容器不对 View 有任何限制,要多大有多大。常用于系统内部。
      • EXACTLY(精确模式):父视图为子视图指定一个确切的尺寸 SpecSize。对应 LyaoutParams 中的match_parent 或具体数值。
      • AT_MOST(最大模式):父容器为子视图指定一个最大尺寸 SpecSize,View 的大小不能大于这个值。对应LayoutParams 中的 wrap_content。
    • 决定因素:值由子View 的布局参数 LayoutParams 和父容器的MeasureSpec 值共同决定。
      具体规则见下图:

    measure.png

    AsyncTask 的原理

    • AsyncTask 中有两个线程池(SerialExecutor 和THREAD_POOL_EXECUTOR)和一个 Handler(InternalHandler),其中线程池 SerialExecutor 用于任务
      的排队,而线程池 THREAD_POOL_EXECUTOR 用于真正地执行任务,InternalHandler 用于将执行环境从线程池切换到主线程。

    • sHandler 是一个静态的 Handler 对象,为了能够将执行环境切换到主线程,这就要求 sHandler 这个对象必须在主线程创建。由于静态成员会在加载类的时候进行初始化,因此这就变相要求 AsyncTask 的类必须在主线程中加载,否则同一个进程
      中的 AsyncTask 都将无法正常工作。

    SurfaceView是什么?他的继承方式是什么?他与View的区别

    SurfaceView中采用了双缓冲机制,保证了UI界面的流畅性,
    同时SurfaceView不在主线程中绘制,而是另开辟一个线程去绘制,所以它不妨碍UI线程;
    SurfaceView继承于View,他和View主要有以下三点区别:
    (1)View底层没有双缓冲机制,SurfaceView有;
    (2)view主要适用于主动更新,而SurfaceView适用与被动的更新,如频繁的刷新
    (3)view会在主线程中去更新UI,而SurfaceView则在子线程中刷新;
    SurfaceView的内容不在应用窗口上,所以不能使用变换(平移、缩放、旋转等)。也难以放在ListView或者ScrollView中,不能使用UI控件的一些特性比如View.setAlpha()

    View:显示视图,内置画布,提供图形绘制函数、触屏事件、按键事件函数等;必须在UI主线程内更新画面,速度较慢。
    SurfaceView:基于view视图进行拓展的视图类,更适合2D游戏的开发;是view的子类,类似使用双缓机制,在新的线程中更新画面所以刷新界面速度比view快,Camera预览界面使用SurfaceView。
    GLSurfaceView:基于SurfaceView视图再次进行拓展的视图类,专用于3D游戏开发的视图;是SurfaceView的子类,openGL专用。

    Activity-Window-View三者的差别

    Activity:是安卓四大组件之一,负责界面展示、用户交互与业务逻辑处理;
    Window:就是负责界面展示以及交互的职能部门,就相当于Activity的下属,Activity的生命周期方法负责业务的处理;
    View:就是放在Window容器的元素,Window是View的载体,View是Window的具体展示。
    三者的关系: Activity通过Window来实现视图元素的展示,window可以理解为一个容器,盛放着一个个的view,用来执行具体的展示工作。

    activity-window-view.png

    如何优化自定义View

    • 1)不要在onDraw或是onLayout()中去创建对象,因为onDraw()方法可能会被频繁调用,可以在view的构造函数中进行创建对象;
    • 2)降低view的刷新频率,尽可能减少不必要的调用invalidate()方法。或是调用带四种参数不同类型的invalidate(),而不是调用无参的方法。无参变量需要刷新整个view,而带参数的方法只需刷新指定部分的view。在onDraw()方法中减少冗余代码。
    • 3)使用硬件加速,GPU硬件加速可以带来性能增加。
    • 4)状态保存与恢复,如果因内存不足,Activity置于后台被杀重启时,View应尽可能保存自己属性,可以重写onSaveInstanceState和onRestoreInstanceState方法,状态保存。

    讲解一下Context

    Context是一个抽象基类。在翻译为上下文,也可以理解为环境,是提供一些程序的运行环境基础信息。Context下有两个子类,ContextWrapper是上下文功能的封装类,而ContextImpl则是上下文功能的实现类。而ContextWrapper又有三个直接的子类, ContextThemeWrapper、Service和Application。其中,ContextThemeWrapper是一个带主题的封装类,而它有一个直接子类就是Activity,所以Activity和Service以及Application的Context是不一样的,只有Activity需要主题,Service不需要主题。Context一共有三种类型,分别是Application、Activity和Service。这三个类虽然分别各种承担着不同的作用,但它们都属于Context的一种,而它们具体Context的功能则是由ContextImpl类去实现的,因此在绝大多数场景下,Activity、Service和Application这三种类型的Context都是可以通用的。不过有几种场景比较特殊,比如启动Activity,还有弹出Dialog。出于安全原因的考虑,Android是不允许Activity或Dialog凭空出现的,一个Activity的启动必须要建立在另一个Activity的基础之上,也就是以此形成的返回栈。而Dialog则必须在一个Activity上面弹出(除非是System Alert类型的Dialog),因此在这种场景下,我们只能使用Activity类型的Context,否则将会出错。

    getApplicationContext()和getApplication()方法得到的对象都是同一个application对象,只是对象的类型不一样。

    Context数量 = Activity数量 + Service数量 + 1 (1为Application)

    View的绘制流程

    View的绘制流程:OnMeasure()——>OnLayout()——>OnDraw()

    第一步:OnMeasure():测量视图大小。从顶层父View到子View递归调用measure方法,measure方法又回调OnMeasure。

    第二步:OnLayout():确定View位置,进行页面布局。从顶层父View向子View的递归调用view.layout方法的过程,即父View根据上一步measure子View所得到的布局大小和布局参数,将子View放在合适的位置上

    第三步:OnDraw():绘制视图。ViewRoot创建一个Canvas对象,然后调用OnDraw()。六个步骤:①、绘制视图的背景;②、保存画布的图层(Layer);③、绘制View的内容;④、绘制View子视图,如果没有就不用;⑤、还原图层(Layer);⑥、绘制滚动条。

    Handler的原理

    View,ViewGroup事件分发

    1. Touch事件分发中只有两个主角:ViewGroup和View。ViewGroup包含onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent三个相关事件。View包含dispatchTouchEvent、onTouchEvent两个相关事件。其中ViewGroup又继承于View。

      2.ViewGroup和View组成了一个树状结构,根节点为Activity内部包含的一个ViwGroup。

      3.触摸事件由Action_Down、Action_Move、Aciton_UP组成,其中一次完整的触摸事件中,Down和Up都只有一个,Move有若干个,可以为0个。

      4.当Acitivty接收到Touch事件时,将遍历子View进行Down事件的分发。ViewGroup的遍历可以看成是递归的。分发的目的是为了找到真正要处理本次完整触摸事件的View,这个View会在onTouchuEvent结果返回true。

      5.当某个子View返回true时,会中止Down事件的分发,同时在ViewGroup中记录该子View。接下去的Move和Up事件将由该子View直接进行处理。由于子View是保存在ViewGroup中的,多层ViewGroup的节点结构时,上级ViewGroup保存的会是真实处理事件的View所在的ViewGroup对象:如ViewGroup0-ViewGroup1-TextView的结构中,TextView返回了true,它将被保存在ViewGroup1中,而ViewGroup1也会返回true,被保存在ViewGroup0中。当Move和UP事件来时,会先从ViewGroup0传递至ViewGroup1,再由ViewGroup1传递至TextView。

      6.当ViewGroup中所有子View都不捕获Down事件时,将触发ViewGroup自身的onTouch事件。触发的方式是调用super.dispatchTouchEvent函数,即父类View的dispatchTouchEvent方法。在所有子View都不处理的情况下,触发Acitivity的onTouchEvent方法。

      7.onInterceptTouchEvent有两个作用:1.拦截Down事件的分发。2.中止Up和Move事件向目标View传递,使得目标View所在的ViewGroup捕获Up和Move事件。

    Android中跨进程通讯的几种方式

    Android 跨进程通信,像intent,contentProvider,广播,service都可以跨进程通信。

    intent:这种跨进程方式并不是访问内存的形式,它需要传递一个uri,比如说打电话。

    contentProvider:这种形式,是使用数据共享的形式进行数据共享。

    service:远程服务,aidl

    广播

    AIDL理解

    AIDL: 每一个进程都有自己的Dalvik VM实例,都有自己的一块独立的内存,都在自己的内存上存储自己的数据,执行着自己的操作,都在自己的那片狭小的空间里过完自己的一生。而aidl就类似与两个进程之间的桥梁,使得两个进程之间可以进行数据的传输,跨进程通信有多种选择,比如 BroadcastReceiver , Messenger 等,但是 BroadcastReceiver 占用的系统资源比较多,如果是频繁的跨进程通信的话显然是不可取的;Messenger 进行跨进程通信时请求队列是同步进行的,无法并发执行。

    Binder机制原理

    在Android系统的Binder机制中,是有Client,Service,ServiceManager,Binder驱动程序组成的,其中Client,service,Service Manager运行在用户空间,Binder驱动程序是运行在内核空间的。而Binder就是把这4种组件粘合在一块的粘合剂,其中核心的组件就是Binder驱动程序,Service Manager提供辅助管理的功能,而Client和Service正是在Binder驱动程序和Service Manager提供的基础设施上实现C/S 之间的通信。其中Binder驱动程序提供设备文件/dev/binder与用户控件进行交互,Client、Service,Service Manager通过open和ioctl文件操作相应的方法与Binder驱动程序进行通信。而Client和Service之间的进程间通信是通过Binder驱动程序间接实现的。而Binder Manager是一个守护进程,用来管理Service,并向Client提供查询Service接口的能力。

    ANR

    ANR全名Application Not Responding, 也就是"应用无响应". 当操作在一段时间内系统无法处理时, 系统层面会弹出上图那样的ANR对话框.

    产生原因:

    (1)5s内无法响应用户输入事件(例如键盘输入, 触摸屏幕等).

    (2)BroadcastReceiver在10s内无法结束

    (3)Service 20s内无法结束(低概率)

    解决方式:

    (1)不要在主线程中做耗时的操作,而应放在子线程中来实现。如onCreate()和onResume()里尽可能少的去做创建操作。

    (2)应用程序应该避免在BroadcastReceiver里做耗时的操作或计算。

    (3)避免在Intent Receiver里启动一个Activity,因为它会创建一个新的画面,并从当前用户正在运行的程序上抢夺焦点。

    (4)service是运行在主线程的,所以在service中做耗时操作,必须要放在子线程中。

    Android中的Socket

    Socket通信方式也是C/S架构,比Binder简单很多。在Android系统中采用Socket通信方式的主要有:

    • zygote:用于孵化进程,system_server创建进程是通过socket向zygote进程发起请求;
    • installd:用于安装App的守护进程,上层PackageManagerService很多实现最终都是交给它来完成;
    • lmkd:lowmemorykiller的守护进程,Java层的LowMemoryKiller最终都是由lmkd来完成;
    • adbd:这个也不用说,用于服务adb;
    • logcatd:这个不用说,用于服务logcat;
    • vold:即volume Daemon,是存储类的守护进程,用于负责如USB、Sdcard等存储设备的事件处理。

    等等还有很多,这里不一一列举,Socket方式更多的用于Android framework层与native层之间的通信。Socket通信方式相对于binder比较简单

    4.2 Android组件相关:

    ARouter 路由原理:

    ARouter 维护了一个路由表 Warehouse,其中保存着全部的模块跳转关系,ARouter 路由跳转实际上还是调用了 startActivity 的跳转,使用了原生的Framework 机制,只是通过 apt 注解的形式制造出跳转规则,并人为地拦截跳转和设置跳转条件。

    热修复的原理

    我们知道Java虚拟机 —— JVM 是加载类的class文件的,而Android虚拟机——Dalvik/ART VM 是加载类的dex文件,而他们加载类的时候都需要ClassLoader,ClassLoader有一个子类BaseDexClassLoader,而BaseDexClassLoader下有一个数组——DexPathList,是用来存放dex文件,当BaseDexClassLoader通过调用findClass方法时,实际上就是遍历数组,找到相应的dex文件,找到,则直接将它return。而热修复的解决方法就是将新的dex添加到该集合中,并且是在旧的dex的前面,所以就会优先被取出来并且return返回。

    4.3 性能优化:

    内存溢出(OOM)和内存泄露(对象无法被回收)的区别。

    内存溢出 out of memory:是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。内存溢出通俗的讲就是内存不够用。

    内存泄露 memory leak:是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光

    内存泄露原因

    一、Handler 引起的内存泄漏。

    解决:将Handler声明为静态内部类,就不会持有外部类SecondActivity的引用,其生命周期就和外部类无关,

    如果Handler里面需要context的话,可以通过弱引用方式引用外部类

    二、单例模式引起的内存泄漏。

    解决:Context是ApplicationContext,由于ApplicationContext的生命周期是和app一致的,不会导致内存泄漏

    三、非静态内部类创建静态实例引起的内存泄漏。

    解决:把内部类修改为静态的就可以避免内存泄漏了

    四、非静态匿名内部类引起的内存泄漏。

    解决:将匿名内部类设置为静态的。

    五、注册/反注册未成对使用引起的内存泄漏。

    注册广播接受器、EventBus等,记得解绑。

    六、资源对象没有关闭引起的内存泄漏。

    在这些资源不使用的时候,记得调用相应的类似close()、destroy()、recycler()、release()等方法释放。

    七、集合对象没有及时清理引起的内存泄漏。

    通常会把一些对象装入到集合中,当不使用的时候一定要记得及时清理集合,让相关对象不再被引用。

    八、属性动画造成内存泄露

    动画同样是一个耗时任务,比如在 Activity 中启动了属性动画(ObjectAnimator),但是在销毁 的时候,没有调用 cancle 方法,虽然我们看不到动画了,但是这个动画依然会不断地播放下去, 动画引用所在的控件,所在的控件引用 Activity,这就造成 Activity 无法正常释放。因此同样要 在 Activity 销毁的时候 cancel 掉属性动画,避免发生内存泄漏。

    九、WebView 造成内存泄露

    关于 WebView 的内存泄露,因为 WebView 在加载网页后会长期占用内存而不能被释放,因此我 们在 Activity 销毁后要调用它的 destory()方法来销毁它以释放内存。另外在查阅 WebView 内存泄露相关资料时看到这种情况:

    Webview 下面的 Callback 持有 Activity 引用,造成 Webview 内存无法释放,即使是调用了 Webview.destory()等方法都无法解决问题(Android5.1 之后)。

    最终的解决方案是:在销毁 WebView 之前需要先将 WebView 从父容器中移除,然后在销毁 WebView。

    APP优化项

    app优化:(工具:Hierarchy Viewer 分析布局 工具:TraceView 测试分析耗时的)

    App启动优化(针对于冷启动)

    Application的onCreate(特别是第三方SDK初始化),首屏Activity的渲染都不要进行耗时操作,如果有,就可以放到子线程或者IntentService中

    布局优化

    尽量不要过于复杂的嵌套。可以使用<include>,<merge>,<ViewStub>

    响应优化

    Android系统每隔16ms会发出VSYNC信号重绘我们的界面(Activity)。

    页面卡顿的原因:

    (1)过于复杂的布局.

    (2)UI线程的复杂运算

    (3)频繁的GC,导致频繁GC有两个原因:1、内存抖动, 即大量的对象被创建又在短时间内马上被释放.2、瞬间产生大量的对象会严重占用内存区域。

    内存优化(使用工具:Batterystats & bugreport)

    参考内存泄露和内存溢出部分

    电池使用优化

    (1)优化网络请求

    (2)定位中使用GPS, 请记得及时关闭

    网络优化

    网络优化(网络连接对用户的影响:流量,电量,用户等待)可在Android studio下方logcat旁边那个工具Network Monitor检测

    API设计:App与Server之间的API设计要考虑网络请求的频次, 资源的状态等. 以便App可以以较少的请求来完成业务需求和界面的展示.

    Gzip压缩:使用Gzip来压缩request和response, 减少传输数据量, 从而减少流量消耗.

    图片的Size:可以在获取图片时告知服务器需要的图片的宽高, 以便服务器给出合适的图片, 避免浪费.

    网络缓存:适当的缓存, 既可以让我们的应用看起来更快, 也能避免一些不必要的流量消耗.

    4.4 网络:

    描述一次网络请求的流程

    • 1)域名解析

      浏览器会先搜索自身DNS缓存且对应的IP地址没有过期;若未找到则搜索操作系统自身的DNS缓存;若还未找到则读本地的hotsts文件;还未找到会在TCP/IP设置的本地DNS服务器上找,如果要查询的域名在本地配置的区域资源中,则完成解析;否则根据本地DNS服务器会请求根DNS服务器;根DNS服务器是13台根DNS,会一级一级往下找。

    • TCP三次握手

      客户端先发送SYN=1,ACK=0,序列号seq=x报文;(SYN在连接建立时用来同步序号,SYN=1,ACK=0代表这是一个连接请求报文,对方若同意建立连接,则应在响应报文中使SYN=1,ACK=1)

      服务器返回SYN=1,ACK=1,seq=y, ack=x+1;

      客户端再一次确认,但不用SYN了,回复服务端, ACK=1, seq=x+1, ack=y+1

    • 3)建立TCP连接后发起HTTP请求

      客户端按照指定的格式开始向服务端发送HTTP请求,HTTP请求格式由四部分组成,分别是请求行、请求头、空行、消息体,服务端接收到请求后,解析HTTP请求,处理完成逻辑,最后返回一个具有标准格式的HTTP响应给客户端。

    • 4)服务器响应HTTP请求

      服务器接收处理完请求后返回一个HTTP响应消息给客户端,HTTP响应信息格式包括:状态行、响应头、空行、消息体

    • 5)浏览器解析HTML代码,请求HTML代码中的资源

      浏览器拿到html文件后,就开始解析其中的html代码,遇到js/css/image等静态资源时,向服务器发起一个http请求,如果返回304状态码,浏览器会直接读取本地的缓存文件。否则开启线程向服务器请求下载。

    • 6)浏览器对页面进行渲染并呈现给用户

    • 7)TCP的四次挥手

      当客户端没有东西要发送时就要释放连接(提出中断连接可以是Client也可以是Server),客户端会发送一个FIN=1的没有数据的报文,进入FIN_WAIT状态,服务端收到后会给客户端一个确认,此时客户端不能发送数据,但可接收信息。

    Http位于TCP/IP模型中的第几层?为什么说Http是可靠的数据传输协议?

    tcp/ip的五层模型:

    从下到上:物理层->数据链路层->网络层->传输层->应用层

    其中tcp/ip位于模型中的网络层,处于同一层的还有ICMP(网络控制信息协议)。http位于模型中的应用层

    由于tcp/ip是面向连接的可靠协议,而http是在传输层基于tcp/ip协议的,所以说http是可靠的数据传输协议。

    TCP为什么三次握手不是两次握手,为什么两次握手不安全

    为了实现可靠数据传输, TCP 协议的通信双方,都必须维护一个序列号, 以标识发送出去的数据包中,哪些是已经被对方收到的。
    三次握手的过程即是通信双方相互告知序列号起始值,并确认对方已经收到了序列号起始值的必经步骤。
    如果只是两次握手, 至多只有连接发起方的起始序列号能被确认,另一方选择的序列号则得不到确认

    TCP和UDP的区别

    tcp是面向连接的,由于tcp连接需要三次握手,所以能够最低限度的降低风险,保证连接的可靠性。

    udp 不是面向连接的,udp建立连接前不需要与对象建立连接,无论是发送还是接收,都没有发送确认信号。所以说udp是不可靠的。

    由于udp不需要进行确认连接,使得UDP的开销更小,传输速率更高,所以实时行更好。

    socket和http的区别:

    HTTP 协议:超文本传输协议,对应于应用层,用于如何封装数据.

    TCP/UDP 协议:传输控制协议,对应于传输层,主要解决数据在网络中的传输。

    IP 协议:对应于网络层,同样解决数据在网络中的传输。

    传输数据的时候只使用 TCP/IP 协议(传输层),如果没有应用层来识别数据内容,传输后的协议都是无用的。

    应用层协议很多 FTP,HTTP,TELNET等,可以自己定义应用层协议。

    web 使用 HTTP 作传输层协议,以封装 HTTP 文本信息,然后使用 TCP/IP 做传输层协议,将数据发送到网络上。

    一、HTTP 协议

    http 为短连接:客户端发送请求都需要服务器端回送响应.请求结束后,主动释放链接,因此为短连接。通常的做法是,不需要任何数据,也要保持每隔一段时间向服务器发送"保持连接"的请求。这样可以保证客户端在服务器端是"上线"状态。

    HTTP连接使用的是"请求-响应"方式,不仅在请求时建立连接,而且客户端向服务器端请求后,服务器才返回数据。

    二、Socket 连接

    要想明白 Socket,必须要理解 TCP 连接。

    TCP 三次握手:握手过程中并不传输数据,在握手后服务器与客户端才开始传输数据,理想状态下,TCP 连接一旦建立,在通讯双方中的任何一方主动断开连接之前 TCP 连接会一直保持下去。

    Socket 是对 TCP/IP 协议的封装,Socket 只是个接口不是协议,通过 Socket 我们才能使用 TCP/IP 协议,除了 TCP,也可以使用 UDP 协议来传递数据。

    创建 Socket 连接的时候,可以指定传输层协议,可以是 TCP 或者 UDP,当用 TCP 连接,该Socket就是个TCP连接,反之。

    Socket 原理

    Socket 连接,至少需要一对套接字,分为 clientSocket,serverSocket 连接分为3个步骤:

    (1) 服务器监听:服务器并不定位具体客户端的套接字,而是时刻处于监听状态;

    (2) 客户端请求:客户端的套接字要描述它要连接的服务器的套接字,提供地址和端口号,然后向服务器套接字提出连接请求;

    (3) 连接确认:当服务器套接字收到客户端套接字发来的请求后,就响应客户端套接字的请求,并建立一个新的线程,把服务器端的套接字的描述发给客户端。一旦客户端确认了此描述,就正式建立连接。而服务器套接字继续处于监听状态,继续接收其他客户端套接字的连接请求.

    Socket为长连接:通常情况下Socket 连接就是 TCP 连接,因此 Socket 连接一旦建立,通讯双方开始互发数据内容,直到双方断开连接。在实际应用中,由于网络节点过多,在传输过程中,会被节点断开连接,因此要通过轮询高速网络,该节点处于活跃状态。

    很多情况下,都是需要服务器端向客户端主动推送数据,保持客户端与服务端的实时同步。

    若双方是 Socket 连接,可以由服务器直接向客户端发送数据。

    若双方是 HTTP 连接,则服务器需要等客户端发送请求后,才能将数据回传给客户端。

    因此,客户端定时向服务器端发送请求,不仅可以保持在线,同时也询问服务器是否有新数据,如果有就将数据传给客户端。

    Socket适用场景:网络游戏,银行交互,支付。

    http适用场景:公司OA服务,互联网服务。

    Socket建立网络连接的步骤

    建立Socket连接至少需要一对套接字,其中一个运行与客户端--ClientSocket,一个运行于服务端--ServiceSocket

    1、服务器监听:服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求。

    2、客户端请求:指客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。注意:客户端的套接字必须描述他要连接的服务器的套接字,

    指出服务器套接字的地址和端口号,然后就像服务器端套接字提出连接请求。

    3、连接确认:当服务器端套接字监听到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述

    发给客户端,一旦客户端确认了此描述,双方就正式建立连接。而服务端套接字则继续处于监听状态,继续接收其他客户端套接字的连接请求。

    http和https的区别

    1、https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。

    2、http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。

    3、http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。

    4、http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。

    https实现原理:

    (1)客户使用https的URL访问Web服务器,要求与Web服务器建立SSL连接。

    (2)Web服务器收到客户端请求后,会将网站的证书信息(证书中包含公钥)传送一份给客户端。

    (3)客户端的浏览器与Web服务器开始协商SSL连接的安全等级,也就是信息加密的等级。

    (4)客户端的浏览器根据双方同意的安全等级,建立会话密钥,然后利用网站的公钥将会话密钥加密,并传送给网站。

    (5)Web服务器利用自己的私钥解密出会话密钥。

    (6)Web服务器利用会话密钥加密与客户端之间的通信。

    HttpClient与HttpUrlConnection的区别

    此处延伸:Volley里用的哪种请求方式(2.3前HttpClient,2.3后HttpUrlConnection)

    • HttpClient和HttpUrlConnection 这两种方式都支持Https协议,都是以流的形式进行上传或者下载数据,也可以说是以流的形式进行数据的传输,还有ipv6,以及连接池等功能。
    • HttpClient这个拥有非常多的API,所以如果想要进行扩展的话,并且不破坏它的兼容性的话,很难进行扩展,也就是这个原因,Google在Android6.0的时候,直接就弃用了这个HttpClient.
    • 而HttpUrlConnection相对来说就是比较轻量级了,API比较少,容易扩展,并且能够满足Android大部分的数据传输。比较经典的一个框架volley,在2.3版本以前都是使用HttpClient,在2.3以后就使用了HttpUrlConnection。

    Volley返回数据量比较大的请求时怎么办

    volley中为了提高请求处理的速度,采用了ByteArrayPool进行内存中的数据存储的,如果下载大量的数据,这个存储空间就会溢出,所以不适合大量的数据。

    BasicNetwork是volley处理返回response的默认实现,它是把server返回的流全部导入内存,你说,下载大数据会用它吗?ByteArrayPool只是一个小于4k的内存缓存池,它被只是用在了BasicNetwork的实现里。上传和BasicNetwork应该没有多大关系,volley也是可以上传大数据的,volley也是可以下载大数据的,只是你不要使用BasicNetwork就行了

    todo:Okhttp源码:

    todo:RxJava源码:

    4.5 安全机制:

    Android 的签名机制?

    • Android 的签名机制包含有 消息摘要、 数字签名数字证
      • 消息摘要:在消息数据上,执行一个单向的 Hash 函
        数,生成一个固定长度的 Hash 值
      • 数字签名:一种以电子形式存储消息签名的方法,
        一个完整的数字签名方案应该由两部分组成: 签名
        算法和验证算法
      • 数字证书:一个经证书授权(Certificate
        Authentication)中心数字签名的包含公钥拥有者
        信息以及公钥的文件

    v3 签名key和v2还有v1有什么区别

    sign.png
    • v1版本的签名中,签名以文件的形式存在于 apk 包中,
      这个版本的 apk 包就是一个标准的 zip 包,v2v1的差
      别是v2是对整个 zip 包进行签名,而且在 zip 包中增加了
      一个 apk signature block,里面保存签名信息。
    • v2版本签名块(APK Signing Block)本身又主要分成三部
      分:
    • SignerData(签名者数据):主要包括签名者的证
      书,整个 APK 完整性校验 hash,以及一些必要信息
    • Signature(签名):开发者对 SignerData 部分数
      据的签名数据
    • PublicKey(公钥):用于验签的公钥数据
    • v3版本签名块也分成同样的三部分,与 v2 不同的是在
      SignerData 部分,v3 新增了 attr 块,其中是由更小的
      level 块组成。每个 level 块中可以存储一个证书信息。
      前一个 level 块证书验证下一个 level 证书,以此类推。
      最后一个 level 块的证书,要符合 SignerData 中本身的证
      书,即用来签名整个 APK 的公钥所属于的证书

    如何绕过9.0限制

    9.0.png

    如何实现进程保活

    • Service设置成START_STICKY kill 后会被重启(等待5秒左右),重传Intent,保持与重启前一样
    • 通过 startForeground将进程设置为前台进程, 做前台服务,优先级和前台应用一个级别,除非在系统内存非常缺,否则此进程不会被 kill
    • 双进程Service: 让2个进程互相保护对方,其中一个Service被清理后,另外没被清理的进程可以立即重启进程
    • 用C编写守护进程(即子进程) : Android系统中当前进程(Process)fork出来的子进程,被系统认为是两个不同的进程。当父进程被杀死的时候,子进程仍然可以存活,并不受影响(Android5.0以上的版本不可行)联系厂商,加入白名单
    • 锁屏状态下,开启一个一像素Activity

    4.6 Android各个版本

    Android5.0~10.0 之间大的变化

    • Android5.0 新特性
      • MaterialDesign 设计风格
      • 支持 64 位ART 虚拟机(5.0 推出的 ART 虚拟机,在
        5.0 之前都是 Dalvik。他们的区别是:Dalvik,每次运行,字节码都需要通过即时编译器转换成机器码(JIT)。 ART,第一次安装应用的时候,字节码就会预先编译成机器码(AOT))
      • 通知详情可以用户自己设计
    • Android6.0 新特性
      • 动态权限管理
      • 支持快速充电的切换
      • 支持文件夹拖拽应用
      • 相机新增专业模式
    • Android7.0 新特性
      • 多窗口支持
      • V2 签名
      • 增强的 Java8 语言模式
      • 夜间模式
    • Android8.0 (O)新特性
      • 优化通知:通知渠道 (Notification Channel) 通知标志 休眠 通知超时 通知设置 通知清除
      • 画中画模式:清单中 Activity 设置android:supportsPictureInPicture
      • 后台限制
      • 自动填充框架
      • 系统优化
      • 等等优化很多
    • Android9.0 (P)新特性
      • 室内 WIFI 定位
      • “刘海”屏幕支持
      • 安全增强
      • 等等优化很多
    • Android10.0 (Q)目前曝光的新特性
      • 夜间模式:包括手机上的所有应用都可以为其设置
        暗黑模式。

      • 桌面模式:提供类似于 PC 的体验,但是远远不能代
        替 PC。

      • 屏幕录制:通过长按“电源”菜单中的"屏幕快照"
        来开启。

    Android各个版本API的区别

    android3.0 代号Honeycomb, 引入Fragments, ActionBar,属性动画,硬件加速
    android4.0 代号Ice Cream,API14:截图功能,人脸识别,虚拟按键,3D优化驱动
    android5.0 代号Lollipop API21:调整桌面图标及部件透明度等
    android6.0 代号M Marshmallow API23,软件权限管理,安卓支付,指纹支持,App关联,
    android7.0 代号N Preview API24,多窗口支持(不影响Activity生命周期),增加了JIT编译器,引入了新的应用签名方案APK Signature Scheme v2(缩短应用安装时间和更多未授权APK文件更改保护),严格了权限访问
    android8.0 代号O API26,取消静态广播注册,限制后台进程调用手机资源,桌面图标自适应
    android9.0 代号P API27,加强电池管理,系统界面添加了Home虚拟键,提供人工智能API,支持免打扰模式

    4.7 图片相关

    Glide 的源码设计哪里很微妙

    • Glide的生命周期绑定:可以控制图片的加载状态与当前页面的生命周期同步,使整个加载过程随着页面的状态而启动/恢复,停止,销毁
    • Glide的缓存设计:通过(三级缓存,Lru 算法,Bitmap 复用)对 Resource 进行缓存设计
    • Glide的完整加载过程:采用 Engine 引擎类暴露了一系列方法供 Request 操作

    4.8 Handler

    一个线程能否创建多个Handler,Handler跟Looper之间的对应关系 ?

    一个线程能够创建多个Handler,Handler跟Looper没有对应关系,线程才跟Looper有对应关系,一个线程对应着一个Looper

    handler.png

    Handler机制包含

    消息机制主要包含:

    • Message:消息分为硬件产生的消息(如按钮、触摸)和软件生成的消息;
    • MessageQueue:消息队列的主要功能向消息池投递消息(MessageQueue.enqueueMessage)和取走消息池的消息(MessageQueue.next);
    • Handler:消息辅助类,主要功能向消息池发送各种消息事件(Handler.sendMessage)和处理相应消息事件(Handler.handleMessage);
    • Looper:不断循环执行(Looper.loop),按分发机制将消息分发给目标处理者。

    Handler架构图:

    • Looper有一个MessageQueue消息队列;
    • MessageQueue有一组待处理的Message;
    • Message中有一个用于处理消息的Handler;
    • Handler中有Looper和MessageQueue。
    handler-frame.jpg

    Looper

    prepare()

    对于无参的情况,默认调用prepare(true),表示的是这个Looper允许退出,而对于false的情况则表示当前Looper不允许退出。

    private static void prepare(boolean quitAllowed) {
        //每个线程只允许执行一次该方法,第二次执行时线程的TLS已有数据,则会抛出异常。
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        //创建Looper对象,并保存到当前线程的TLS区域
        sThreadLocal.set(new Looper(quitAllowed));
    }
    

    这里的sThreadLocal是ThreadLocal类型,下面,先说说ThreadLocal。

    ThreadLocal

    ThreadLocal: 线程本地存储区(Thread Local Storage,简称为TLS),每个线程都有自己的私有的本地存储区域,不同线程之间彼此不能访问对方的TLS区域。TLS常用的操作方法:

    • ThreadLocal.set(T value):将value存储到当前线程的TLS区域,源码如下:
    public void set(T value) {
        Thread currentThread = Thread.currentThread(); //获取当前线程
        Values values = values(currentThread); //查找当前线程的本地储存区
        if (values == null) {
            //当线程本地存储区,尚未存储该线程相关信息时,则创建Values对象
            values = initializeValues(currentThread);
        }
        //保存数据value到当前线程this
        values.put(this, value);
    }
    
    • ThreadLocal.get():获取当前线程TLS区域的数据,源码如下:
    public T get() {
        Thread currentThread = Thread.currentThread(); //获取当前线程
        Values values = values(currentThread); //查找当前线程的本地储存区
        if (values != null) {
            Object[] table = values.table;
            int index = hash & values.mask;
            if (this.reference == table[index]) {
                return (T) table[index + 1]; //返回当前线程储存区中的数据
            }
        } else {
            //创建Values对象
            values = initializeValues(currentThread);
        }
        return (T) values.getAfterMiss(this); //从目标线程存储区没有查询是则返回null
    }
    

    ThreadLocal的get()和set()方法操作的类型都是泛型,接着回到前面提到的sThreadLocal变量,其定义如下:

    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>()
    

    可见sThreadLocal的get()和set()操作的类型都是Looper类型。

    Looper.prepare()

    Looper.prepare()在每个线程只允许执行一次,该方法会创建Looper对象,Looper的构造方法中会创建一个MessageQueue对象,再将Looper对象保存到当前线程TLS。

    对于Looper类型的构造方法如下:

    private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);  //创建MessageQueue对象. 【见4.1】
        mThread = Thread.currentThread();  //记录当前线程.
    }
    

    另外,与prepare()相近功能的,还有一个prepareMainLooper()方法,该方法主要在ActivityThread类中使用。

    public static void prepareMainLooper() {
        prepare(false); //设置不允许退出的Looper
        synchronized (Looper.class) {
            //将当前的Looper保存为主Looper,每个线程只允许执行一次。
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }
    

    Looper.loop()

    loop()进入循环模式,不断重复下面的操作,直到没有消息时退出循环

    • 读取MessageQueue的下一条Message;
    • 把Message分发给相应的target;
    • 再把分发后的Message回收到消息池,以便重复利用。

    Handler

    无参构造

    对于Handler的无参构造方法,默认采用当前线程TLS中的Looper对象,并且callback回调方法为null,且消息为同步处理方式。只要执行的Looper.prepare()方法,那么便可以获取有效的Looper对象。

    public Handler() {
        this(null, false);
    }
    
    public Handler(Callback callback, boolean async) {
        //匿名类、内部类或本地类都必须申明为static,否则会警告可能出现内存泄露
        if (FIND_POTENTIAL_LEAKS) {
            final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            }
        }
        //必须先执行Looper.prepare(),才能获取Looper对象,否则为null.
        mLooper = Looper.myLooper();  //从当前线程的TLS中获取Looper对象【见2.1】
        if (mLooper == null) {
            throw new RuntimeException("");
        }
        mQueue = mLooper.mQueue; //消息队列,来自Looper对象
        mCallback = callback;  //回调方法
        mAsynchronous = async; //设置消息是否为异步处理方式
    }
    

    有参构造

    Handler类在构造方法中,可指定Looper,Callback回调方法以及消息的处理方式(同步或异步),对于无参的handler,默认是当前线程的Looper。

    public Handler(Looper looper) {
        this(looper, null, false);
    }
    
    public Handler(Looper looper, Callback callback, boolean async) {
        mLooper = looper;
        mQueue = looper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }
    

    消息分发机制

    分发消息流程:

    1. Message的回调方法不为空时,则回调方法msg.callback.run(),其中callBack数据类型为Runnable,否则进入步骤2;
    2. HandlermCallback成员变量不为空时,则回调方法mCallback.handleMessage(msg),否则进入步骤3;
    3. 调用Handler自身的回调方法handleMessage(),该方法默认为空,Handler子类通过覆写该方法来完成具体的逻辑。

    对于很多情况下,消息分发后的处理方法是第3种情况,即Handler.handleMessage(),一般地往往通过覆写该方法从而实现自己的业务逻辑。

    在Looper.loop()中,当发现有消息时,调用消息的目标handler,执行dispatchMessage()方法来分发消息。

    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            //当Message存在回调方法,回调msg.callback.run()方法;
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                //当Handler存在Callback成员变量时,回调方法handleMessage();
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            //Handler自身的回调方法handleMessage()
            handleMessage(msg);
        }
    }
    

    消息发送

    java_sendmessage.png

    从上图,可以发现所有的发消息方式,最终都是调用MessageQueue.enqueueMessage();

    小节

    Handler.sendEmptyMessage()等系列方法最终调用MessageQueue.enqueueMessage(msg, uptimeMillis),将消息添加到消息队列中,其中uptimeMillis为系统当前的运行时间,不包括休眠时间。

    obtainMessage

    获取消息

    public final Message obtainMessage() {
        return Message.obtain(this); 【见5.2】
    }
    

    Handler.obtainMessage()方法,最终调用Message.obtainMessage(this),其中this为当前的Handler对象。

    removeMessages

    public final void removeMessages(int what) {
        mQueue.removeMessages(this, what, null); 【见 4.5】
    }
    

    Handler是消息机制中非常重要的辅助类,更多的实现都是MessageQueue, Message中的方法,Handler的目的是为了更加方便的使用消息机制。

    MessageQueue

    next()

    提取下一条message

    nativePollOnce是阻塞操作,其中nextPollTimeoutMillis代表下一个消息到来前,还需要等待的时长;当nextPollTimeoutMillis = -1时,表示消息队列中无消息,会一直等待下去。

    当处于空闲时,往往会执行IdleHandler中的方法。当nativePollOnce()返回后,next()从mMessages中提取一个消息。

    Message next() {
        final long ptr = mPtr;
        if (ptr == 0) { //当消息循环已经退出,则直接返回
            return null;
        }
        int pendingIdleHandlerCount = -1; // 循环迭代的首次为-1
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }
            //阻塞操作,当等待nextPollTimeoutMillis时长,或者消息队列被唤醒,都会返回
            nativePollOnce(ptr, nextPollTimeoutMillis);
            synchronized (this) {
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                //当消息的Handler为空时,则查询异步消息
                if (msg != null && msg.target == null) {
                    //当查询到异步消息,则立刻退出循环
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        //当异步消息触发时间大于当前时间,则设置下一次轮询的超时时长
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // 获取一条消息,并返回
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        //设置消息的使用状态,即flags |= FLAG_IN_USE
                        msg.markInUse();
                        return msg;   //成功地获取MessageQueue中的下一条即将要执行的消息
                    }
                } else {
                    //没有消息
                    nextPollTimeoutMillis = -1;
                }
                //消息正在退出,返回null
                if (mQuitting) {
                    dispose();
                    return null;
                }
                //当消息队列为空,或者是消息队列的第一个消息时
                if (pendingIdleHandlerCount < 0 && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    //没有idle handlers 需要运行,则循环并等待。
                    mBlocked = true;
                    continue;
                }
                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }
            //只有第一次循环时,会运行idle handlers,执行完成后,重置pendingIdleHandlerCount为0.
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; //去掉handler的引用
                boolean keep = false;
                try {
                    keep = idler.queueIdle();  //idle时执行的方法
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }
                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }
            //重置idle handler个数为0,以保证不会再次重复运行
            pendingIdleHandlerCount = 0;
            //当调用一个空闲handler时,一个新message能够被分发,因此无需等待可以直接查询pending message.
            nextPollTimeoutMillis = 0;
        }
    }
    

    enqueueMessage

    添加一条消息到消息队列

    MessageQueue是按照Message触发时间的先后顺序排列的,队头的消息是将要最早触发的消息。当有消息需要加入消息队列时,会从队列头开始遍历,直到找到消息应该插入的合适位置,以保证所有消息的时间顺序。

    boolean enqueueMessage(Message msg, long when) {
        // 每一个普通Message必须有一个target
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + " This message is already in use.");
        }
        synchronized (this) {
            if (mQuitting) {  //正在退出时,回收msg,加入到消息池
                msg.recycle();
                return false;
            }
            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                //p为null(代表MessageQueue没有消息) 或者msg的触发时间是队列中最早的, 则进入该该分支
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked; //当阻塞时需要唤醒
            } else {
                //将消息按时间顺序插入到MessageQueue。一般地,不需要唤醒事件队列,除非
                //消息队头存在barrier,并且同时Message是队列中最早的异步消息。
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p;
                prev.next = msg;
            }
            //消息没有退出,我们认为此时mPtr != 0
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }
    

    removeMessages

    这个移除消息的方法,采用了两个while循环,第一个循环是从队头开始,移除符合条件的消息,第二个循环是从头部移除完连续的满足条件的消息之后,再从队列后面继续查询是否有满足条件的消息需要被移除。

    void removeMessages(Handler h, int what, Object object) {
        if (h == null) {
            return;
        }
        synchronized (this) {
            Message p = mMessages;
            //从消息队列的头部开始,移除所有符合条件的消息
            while (p != null && p.target == h && p.what == what
                   && (object == null || p.obj == object)) {
                Message n = p.next;
                mMessages = n;
                p.recycleUnchecked();
                p = n;
            }
            //移除剩余的符合要求的消息
            while (p != null) {
                Message n = p.next;
                if (n != null) {
                    if (n.target == h && n.what == what
                        && (object == null || n.obj == object)) {
                        Message nn = n.next;
                        n.recycleUnchecked();
                        p.next = nn;
                        continue;
                    }
                }
                p = n;
            }
        }
    }
    

    Message

    消息对象

    每个消息用Message表示,Message主要包含以下内容:

    数据类型 成员变量 解释
    int what 消息类别
    long when 消息触发时间
    int arg1 参数1
    int arg2 参数2
    Object obj 消息内容
    Handler target 消息响应方
    Runnable callback 回调方法

    创建消息的过程,就是填充消息的上述内容的一项或多项。

    消息池

    在代码中,可能经常看到recycle()方法,咋一看,可能是在做虚拟机的gc()相关的工作,其实不然,这是用于把消息加入到消息池的作用。这样的好处是,当消息池不为空时,可以直接从消息池中获取Message对象,而不是直接创建,提高效率。

    静态变量sPool的数据类型为Message,通过next成员变量,维护一个消息池;静态变量MAX_POOL_SIZE代表消息池的可用大小;消息池的默认大小为50。

    消息池常用的操作方法是obtain()和recycle()。

    Message.obtain

    从消息池中获取消息

    obtain(),从消息池取Message,都是把消息池表头的Message取走,再把表头指向next;

    public static Message obtain() {
        synchronized (sPoolSync) {
            if (sPool != null) {
                Message m = sPool;
                sPool = m.next;
                m.next = null; //从sPool中取出一个Message对象,并消息链表断开
                m.flags = 0; // 清除in-use flag
                sPoolSize--; //消息池的可用大小进行减1操作
                return m;
            }
        }
        return new Message(); // 当消息池为空时,直接创建Message对象
    }
    

    Message.recycle

    把不再使用的消息加入消息池

    recycle(),将Message加入到消息池的过程,都是把Message加到链表的表头;

    public void recycle() {
        if (isInUse()) { //判断消息是否正在使用
            if (gCheckRecycle) { //Android 5.0以后的版本默认为true,之前的版本默认为false.
                throw new IllegalStateException("This message cannot be recycled because it is still in use.");
            }
            return;
        }
        recycleUnchecked();
    }
    
    //对于不再使用的消息,加入到消息池
    void recycleUnchecked() {
        //将消息标示位置为IN_USE,并清空消息所有的参数。
        flags = FLAG_IN_USE;
        what = 0;
        arg1 = 0;
        arg2 = 0;
        obj = null;
        replyTo = null;
        sendingUid = -1;
        when = 0;
        target = null;
        callback = null;
        data = null;
        synchronized (sPoolSync) {
            if (sPoolSize < MAX_POOL_SIZE) { //当消息池没有满时,将Message对象加入消息池
                next = sPool;
                sPool = this;
                sPoolSize++; //消息池的可用大小进行加1操作
            }
        }
    }
    

    总结

    handler_java.jpg

    图解:

    • Handler通过sendMessage()发送Message到MessageQueue队列;
    • Looper通过loop(),不断提取出达到触发条件的Message,并将Message交给target来处理;
    • 经过dispatchMessage()后,交回给Handler的handleMessage()来进行相应地处理。
    • 将Message加入MessageQueue时,处往管道写入字符,可以会唤醒loop线程;如果MessageQueue中没有Message,并处于Idle状态,则会执行IdelHandler接口中的方法,往往用于做一些清理性地工作。

    消息分发的优先级:

    1. Message的回调方法:message.callback.run(),优先级最高;
    2. Handler的回调方法:Handler.mCallback.handleMessage(msg),优先级仅次于1;
    3. Handler的默认方法:Handler.handleMessage(msg),优先级最低。

    消息缓存:

    为了提供效率,提供了一个大小为50的Message缓存队列,减少对象不断创建与销毁的过程。

    相关文章

      网友评论

          本文标题:Android面试题总结

          本文链接:https://www.haomeiwen.com/subject/bwsddktx.html