Skip to content

Latest commit

 

History

History
563 lines (408 loc) · 39.3 KB

面试题.md

File metadata and controls

563 lines (408 loc) · 39.3 KB

Android面试题

Java篇

  • java中==和equals和hashCode的区别

    • ==是java的运算符,如果作用于基本数据类型,判断的就是值是否相等,作用于对象时,判断的是对象地址是否相等;无法重载。
    • equals是Object对象的方法,默认实现是直接判断对象是否相等。可以被重写,譬如String类,重写之后的方法,判断的是两个字符串对象的值是否相等。
    • hashCode方法同样是Object的对象方法,为散列表结构提供哈希码支持。当equals方法被重写时,官方文档中明确表示hashCode方法是有必要随之而重写,来提供唯一的hash值。否则在那些使用散列表的数据结构中会出现问题,例如HashMap。

  • int与integer的区别

    • int是基本数据类型,Integer是它的包装类。
    • int的默认值是0,Integer默认值是null,所以Integer要初始化之后才能使用,而int不需要。
    • 在使用==运算符进行比较的时候,如果双方都是int类型的变量,那么比较的就是它们的值;涉及Integer类型的变量需要视情况而定:
      • 比较的双方都是new出来的Integer对象,结果不相等。因为new出来的对象地址不一样。
      • 比较的一方是int类型,一方是Integer类型的时候,Integer变量会被自动拆包成为int类型,最终比较的是它们的值。
      • 比较双方都是通过自动装箱进行初始化的Integer变量,如果值区间都在-128127之间,比较的是两者的值,否则不相等。自动装箱值在-128127之间的变量指向的是常量池中的对象,所以只要值相等都是同一对象。
    • Integer类型的对象使用+-*/等运算符进行计算时会自动拆箱。而int被放到需要对象的容器时也会被自动装箱,例如Map的Value。

  • 谈谈对java多态的理解

    父类引用指向子类对象,执行的是子类的方法。重写是多态的体现。作用是消除类型之间的耦合。好处最大是灵活。是很多设计模式的基础。
    《深入理解Java虚拟机》第八章中方法调用篇讲述了其技术实现。

  • 什么是内部类?内部类的作用

    在Java中,可以将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类。广泛意义上的内部类一般来说包括这四种:成员内部类、局部内部类、匿名内部类和静态内部类。

    作用:
    1. 每个内部类都能独立的继承一个接口的实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。内部类使得多继承的解决方案变得完整。
    2. 方便将存在一定逻辑关系的类组织在一起,又可以对外界隐藏。
    3. 方便代码编写,例如线程中的匿名内部类。

    引申:
    内部类与闭包

  • 抽象类和接口的区别

    • 语法上:

      1. 抽象类可以提供成员方法的具体实现而接口不行(Java1.8以后接口有默认方法,只要方法上加上default关键字,也可以提供具体实现);
      2. 抽象类可以拥有各种访问类型的成员变量,接口只能有public static final类型的;
      3. 接口中不能含有代码块和静态代码块,而抽象类可以;
      4. 一个类只能继承一个抽象类,而一个类却可以实现多个接口;
      5. 抽象子类继承抽象类,可以不实现其抽象方法,接口的实现类必须全部实现其接口方法。
    • 设计上:

      1. 抽象类是对事物的抽象,即对类的抽象,而接口是对一系列对象行为的抽象;
      2. 抽象类为子类提供模板,实现子类提供实现细节,其主体逻辑仍然在抽象类中。

  • 接口的意义

    1. 增强代码规范
    2. 增强代码灵活性,便于拓展和替换
    3. 降低模块耦合
    4. 实现多继承

  • 泛型中extends和super的区别

    在声明变量类型的时候可以使用?通配符来进行泛型的匹配:

    • <? extends T>是上界通配符,匹配的是类型T及其子类。当一个变量使用了上界通配符进行泛型声明时,其指向对象的泛型类型只能是类型T及其子类,即指定了泛型在继承树的最顶端为类型T。由于类型T拥有不能确定的子类数量,当方法需要接收上界为T的泛型类型参数时,编译器无法确认指向对象的泛型类型与传入的参数类型是否一致,所以会导致编译不通过。但在需要输出泛型类型时,其上界已经确定为T,所以上界通配符多数应用在生产者角色上。extends关键字在泛型类和泛型接口中还可以表示泛型的继承关系,与之相关的是implements,表示泛型的接口实现关系,会造成部分混淆。Kotlin中使用out关键字来替代Java中在生产者角色上对上界通配符的使用。
    • <? super T>是下界通配符,匹配的是类型T及其父类。表示类型T在继承树的最低端,所有其父类包括自身都可以指定为实际泛型。由于Java中的多态特性,可以接受T的子类对象。

    使用原则:PECS,生产者使用extends,消费者使用super

  • 父类的静态方法能否被子类重写

    不能,涉及到虚拟机部分,待补充

  • 进程和线程的区别

    • 进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位
    • 每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
    • 通信方式不一样
    • 每个进程至少拥有一个线程

    引申:协程

  • final,finally,finalize的区别
    • final:
      • 修饰一个类的时候,表示当前类不能被派生
      • 修饰一个变量的时候,表示该变量在运行期间不会被改变,所以必须在声明的时候进行初始化
      • 修饰一个方法的时候,表示该方法不能被重写
    • finally:
      异常处理结构中的一部分,该部分总是执行的。
      引申:try块有返回,finally也有返回,会返回哪个?finally
    • finalize:
      finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法。在对象被从内存中清理出去之前,做必要的清理工作。

  • String类为什么是final的

    主要是为了”安全性“和”效率“的缘故,因为:

    1. 由于String类不能被继承,所以就不会没修改,这就避免了因为继承引起的安全隐患;

    2. String类在程序中出现的频率比较高,如果为了避免安全隐患,在它每次出现时都用final来修饰,这无疑会降低程序的执行效率,所以干脆直接将其设为final一提高效率;

       作者:厌倦
       链接:https://zhuanlan.zhihu.com/p/46298955
       来源:知乎
       著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
      

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

    区别:

    1. 静态代理编译时就可以确定被代理类,而动态代理在运行时才能确定;
    2. 静态代理通常只代理一个目标对象,动态代理代理的是某个接口的多实现;
    3. 静态代理类再程序运行前已经存在,动态代理类是程序运行时通过反射机制动态创建。

    场景:

    1. 静态代理多用于对象的访问控制,例如第三方库的核心代码实现。
    2. 动态代理可以实现面向切面编程,例如Spring AOP。

  • Java的异常体系

    Throwable类是所有异常的超类,这些异常分为两大类:Error和Exception。根据异常的处理方式,又被分为编译检查异常(Checked Exception)和不检查异常(Unchecked Exception):

    • Error是程序无法处理的异常,虚拟机一般会停止运行,例如OutOfMemoryError。Error及其子类都是Unchecked Exception,所以我们也不需要对Error进行捕捉。
    • Exception又分为运行时异常(RuntimeException)和非运行时异常。运行时异常及所有RuntimeException的派生异常也是Unchecked Exception,处理时无需强制捕抓。而非运行时异常在处理的时候,如果不进行捕捉或者向上抛出,则无法通过编译。

  • 依赖注入

    A类中有B的实例,称A对B有一个依赖。其中通过构造方法传入依赖对象等外部传入依赖,而非自己主动创建依赖的方式,称之为依赖注入。它的好处主要有:

    1. 解耦
    2. 便于测试

原理及虚拟机篇

  • Java内存模型

    • Java内存模型规定了所有变量都存储在主内存中,每条线程还有自己的工作内存。线程的工作内存中保存了被该线程使用的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,线程间变量值的传递均需要通过主内存来完成。
    • 主内存和工作内存的交互协议:lock、unlock、read、load、assign、use、store、write等。这些指令必须具有原子性。
    • Java内存模型对volatile关键字定义了一些特殊的访问规则:
      • 保证此关键字修饰的变量对所有线程的可见性
      • 禁止对其进行指令重排序优化
    • 允许不保证64位数据类型的read、load、store、write这4个操作的原子性,即long和double的非原子性协定
    • 先行发生原则(happens-before):如果不满足这些规则,也没有使用其他手段处理,那么代码就没有顺序性保障,虚拟机可以对它们进行重排序。包括:
      • 程序次序规则
      • 管程锁定规则
      • volat变量规则
      • 线程启动规则
      • 线程终止规则
      • 线程中断规则
      • 对象终结规则
      • 传递性

    Java内存模型的特征:

    • 原子性
    • 可见性
    • 有序性:
      如果在本线程内观察,所有的操作都是有序的;如果在要一个线程中观察另一个线程,所有的操作都是无序的。

  • Java内存区域

  • 类加载过程

    虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

  • GC Root有哪些

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

  • GC什么情况下会发生

    • 新对象一般在新生代Eden区分配,当Eden区没有足够空间分配时,虚拟机会发起一次Minor GC;
    • 经过Minor GC后仍然存活的对象会放到Survivor空间,在此空间内经过(默认)15此的GC仍然存活,就会被放到老年代;
    • 在Minor GC前,如果老年代最大可用的连续空间小于新生代所有对象的总空间或者历次晋升的平均大小就会就行Full GC。

  • 哪些情况下的对象会被垃圾回收机制处理掉?

    **堆区:**一般具有以下条件:
    1. 强引用,但GC Roots到这个对象不可达;
    2. 软引用,在垃圾回收之后内存依然紧张;
    3. 只被弱引用或者虚引用指向到的对象;
    4. 上述对象会被第一次标记,并且进行二次筛选,筛选的条件是对象是否有必要执行finalize方法。如果对象没有重写该方法,或者之前已经执行过,那么基本会被回收。

    方法区:方法区的垃圾收集主要是回收:废弃常量和无用的类。

  • 谈谈你对解析与分派的认识

    方法调用的目标方法在Class文件里面都是一个常量池的符号引用,在类加载的解析阶段,某些方法在程序运行前就有一个可确定的调用版本,并且运行期间调用版本不可变,那么这部分目标方法会将符号引用转化为直接引用。对这类方法的调用称为解析。符合“编译期可知,运行期不可变”这个要求的方法,主要有静态方法和私有方法、构造器、父类方法、final方法等等。

    分派调用可能是静态的,在编译期间就可以确定方法版本;也可能是动态的,需要运行时确定目标方法调用对象来确认方法版本。根据分派依据的宗量数又可以分为单分派和多分派:

    • 所有依赖静态类型来定位方法的执行版本的分派动作成为静态分派。静态分派的典型应用是方法重载。静态分派发生在编译阶段,所以它并不是由虚拟机执行的。
    • 在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。动态分派的典型应用是方法重写。这也是Java多态特性的实现原理。
    • 在编译阶段编译器的选择过程,也就是静态分派过程中,这时选择目标方法的依据有两点:静态类型和参数类型,所以Java语言的静态分派属于多分派类型。
    • 在运行阶段虚拟机的选择,也就是动态分派时期,能影响分派结果的因素只有方法的接受者的实际类型,所以Java语言的动态分派属于单分派类型。

  • 反射的理解

    虚拟机在类的加载阶段,会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。Java 反射是可以让我们在运行时获取类的函数、属性、父类、接口等 Class 内部信息的机制。通过反射还可以让我们在运行期实例化对象,调用方法,通过调用 get/set 方法获取变量的值,即使方法或属性是私有的的也可以通过反射的形式调用。

  • 注解的理解

    注解是能够添加到Java源代码的语法元数据,它不会改变代码,而是可以使其他工具对其进行处理,或者运行时获得注解的信息。例如编译器和注解处理器。注解可以作用于类、方法、变量、参数甚至包上。根据其生命周期可以分为三类:

    1. 源文件保留(SOURCE),例如声明需要编译器检查的注解
    2. Class文件保留(CLASS),例如需要注解处理器去处理的注解
    3. 运行时保留(RUNTIME),例如需要在运行时反射读取注解信息的注解

  • 泛型原理

    泛型的本质是参数化类型的应用,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。

    Java的泛型只在源码中存在,并进行编译器检查,编译后的字节码中并不存在泛型类型,它的这种实现方式称为类型擦除,是Java语言的一种语法糖。虽然擦除后的字节码指令里面没用泛型信息,但元数据中还是保留了,所以可以通过反射获取泛型类型。

数据结构

  • ArrayList和LinkList的区别

    两者都实现了List接口。ArrayList使用动态数组实现,LinkList使用双向链表实现。由于双方使用的数据结构不一样,所以在运行的效率上各有差异。对比一下它们的主要方法:

    • 插入(add):在队列尾部插入时,ArrayList的效率和LinkList相当,但由于ArrayList存在动态数组扩展的需求,所以大部分情况下效率比LinkList稍低;随机插入的时候,ArrayList需要维持队列的顺序,所以要把插入位置后的数据全部后移一位。而LinkList还是只需要找到对应的节点,修改其链表指向。所以插入方法是LinkList比较高效。
    • 读取(get):在随机访问列表数据方面,ArrayList底层是数组,所以可以直接通过下标访问,非常高效;而LinkList需要遍历链表找到对应位置,效率较低。
    • 移除(remove):和插入方法相似,ArrayList需要耗费额外的时间去维持队列顺序,所以效率比LinkList低。

    在遍历LinkeList时,由于链表每一次访问都要从头(或者从尾)遍历,会造成无用的浪费,使用迭代器的话会有当前访问位置缓存,遍历速度变成了O(n),会加快很多。增强for结构内部使用的是迭代器遍历。

  • HashMap实现原理

    HashMap主干是一个Node数组,数组元素Node同时也是一个实现了Map.Entry接口的单向链表节点,持有一对Key-Value键值对、Key的hash值和一个指向另一个Node节点的引用next。在get和put操作时,HashMap使用哈希函数来确定对应Key在数组中的定位,再对数组进行读写操作。但两个不同的元素通过哈希函数可能会得出相同的储存地址,这种情况被称为哈希冲突,也叫哈希碰撞。当插入操作发生碰撞时,HashMap就会把新的元素打包为一个新的Node节点,让已存在的节点指向新的节点,形成单向链表,也就是说HashMap是由数组+单向链表组成。

    在进行get操作时,如果发生哈希冲突,HashMap会按顺序查找数组当前位置上的链表:如果首元素的Key的hash值相等与目标Key的hash值相等,而且它们地址相等或值相等(equals返回true),则返回首元素;否则往下遍历直至找到对应的目标。从这个判断条件可以明白为什么覆盖一个类的equals方法时,也需要覆盖hashCode方法。因为当这些方法不覆盖的时候,它们的方法签名都是基于对象本身的,只覆盖equals方法的话,只要不是同一个对象,即使他们的equals签名判断它们是相同的,也无法获取到存放的数据。

    调用put方法时,如果发现目前的bucket占用成都超过了设置(或者默认的)loadFactor,就会发生resize。简单的说就是把bucket扩充为2倍,之后重新计算index,把节点再放到新的bucket中。

  • LinkHashMap原理及应用

    LinkHashMap实际上只是在HashMap的基础上,把Node单向链表节点变为双向链表节点,并持有链表的头尾引用,以此链来保存访问数据或插入数据的先后顺序。

    控制记录插入顺序还是访问顺序的标记为是accessOrder,当accessOrder标志位为true时,表示链表记录访问顺序,为false表示记录插入顺序:

    • 在需要记录插入顺序时,put()方法是直接调用HashMap的,但在构建数据节点Node时,LinkHashMap会把新的节点放到链表尾部,从而实现插入顺序保存;
    • 在需要记录读取顺序时,get()方法在取出数据后,会调用私有方法afterNodeAccess(),把访问的数据放到双向链表的尾部。

    因此LinkHashMap中,链表的越靠近头部的数据表示最少访问到,越靠近尾部的数据表示最近访问的频率越高。

    Android兼容包提供了一个缓存类LruCache,实现了LRU(最近最少使用)算法,其内部就是使用了LinkHashMap数据结构记录访问频率。LruCache提供最大缓存设置(单位自定),并提供方法sizeOf来指定单个数据的大小(单位需要和缓存设置的单位一致)。当缓存满时,把最近最少使用的对象从内存中移除,直至存放的数据大小在设定的缓存大小内。从LinkHashMap的原理可知,优先移除的对象是靠近队列头的对象。

  • ConcurrentHashMap的实现原理

    ConcurrentHashMap是HashMap的并发版本,它支持完全并发的读,以及给定数量的并发写。在Java1.8的实现上,利用CAS无锁算法和synchronized保证并发写的线程安全问题。

    CAS就是Compare And Swap的缩写,CAS是一种乐观锁实现算法。所谓乐观锁,就是认为程序发生并发问题的可能性比较低或者不会发生,所以不会加锁,但是在更新的时候会判断是否有其他线程在这之前对数据进行修改,如果没有则直接写入,否则重试直至成功。而CAS的具体操作就是,如果需要更新时,判断当前内存的值和写入前获取到的旧值是否相等,相等即表示没有其他线程在进行并发更新操作,可以直接写入新值,否则视为本次更新失败,需要重试。

    CAS算法涉及到3个参数,内存地址、写入前读到的地址的现有值和要写入的新值。Java语言的标准API并不能直接访问和操作硬件,但依然保留了一个Unsafe类通过JNI调用本地方法来提供对更底层硬件的一些原子性操作。ConcurrentHashMap的CAS算法的核心就是Unsafe类的compareAndSwapObject()方法,可以对给定对象偏移量的内存位置进行对比并赋值的操作,如果更新成功则返回ture,否则返回false。

    ConcurrentHashMap的put方法的插入代码包含在一个无限循环的for循环体中,因为CAS插入失败需要不断重试。其流程如下:

    • 如果数组没有初始化或者没有初始化完成,则进行初始化或者等待初始化完成;
    • 如果通过tabAt方法(实现是使用Unsafe类直接读取数组偏移量内存地址,可以保证读取到最新的值)读取到哈希函数分配的位置上的值是空的话,则直接进行CAS插入,插入成功的话break跳出乐观锁循环,执行后续代码,插入失败则表示有其他线程对当前位置进行了修改,需要进入下一轮循环重试;
    • 如果读取到的分配位置上的节点hash值为MOVED(-1),表示有其他线程正在对Node数组进行扩容,则一起进行扩容;
    • 上述情况都不是的话,表示目标节点上已有对象。跟HashMap一样,该对象是一个单向链表或者红黑树,所以要遍历找到合适的位置进行插入。为了防止在遍历的过程中有其他线程对链表或者树进行修改,所以会对这个对象进行加锁。相比1.7的分段锁机制,1.8的这种对单个节点加锁的方式粒度更小,所以可以提供更好的并发写性能,理论上限是可以进行size()个线程的并发写。在这种情况下,会通过当前对象的hash值来判断它是链表还是二叉树,然后跟HashMap一样,如果是链表,遍历,如果Key已经存在,则进行更新(由于已经上锁,所以这里的更新都不需要用CAS),否则打包节点放到链表尾部。如果是红黑树,则执行红黑树的插入。如果链表长度过长,会转为红黑树。
    • 最后跳出循环,并且执行长度更改和根据情况进行数组扩容。

    ConcurrentHashMap的get方法逻辑与HashMap基本一致,但它在并发环境下也没有加锁,主要因为:

    • 对于数组中的元素,put方法是通过Unsafe类直接进行内存原子性操作,也就是说,只要读在写之后,就可以正确读到;
    • 对于链表中的元素,因为链表的next变量是有volatile修饰的,根据volatile的语义可知,只要next修改了就可以在其他线程正确读到;
    • 对于红黑树中的元素,红黑树的putVal和find方法内部也是采用了乐观锁机制来实现数据同步。

  • CopyOnWriteArrayList实现原理

  • 二叉树遍历

    • 广度优先遍历:
      按照树的形状一层一层遍历
    • 深度优先遍历:
      • 前序遍历:
        根据 根节点->左子树->右子树 的顺序递归查找
      • 中序遍历:
        根据 左子树->根节点->右子树 的顺序递归查找
      • 后序遍历: 根据 左子树->右子树->根节点 的顺序递归查找

  • 红黑树

    红黑树是一种特殊的二叉树,它的5个特性:

    1. 节点要么是黑色,要么是红色。
    2. 根节点是黑色。
    3. 叶子节点Nil节点是黑色。
    4. 如果一个节点是红色,则它的子节点必须是黑色。
    5. 从一个节点到该字节的子孙节点的所有路径上包含相同数目的黑节点。

    红黑树的左旋和右旋:

    • 左旋:
      与该节点的右节点交换父子关系,并成为其左节点。原右节点的左节点成为该节点的右节点。
    • 右旋:
      与该节点的左节点交换父子关系,并称为其右节点。原左节点的右节点称为该节点的左节点。

Android篇

  • Activity的生命周期

    • onCreate:Activity创建时调用
    • onRestart:从不可见状态恢复
    • onStart:可见未获取焦点,不可交互
    • onResume:可见,已获取焦点,可交互
    • onPause:可见,失去焦点
    • onStop:不可见
    • onDestroy:销毁
    • onSaveInstanceState:可能被回收时调用
    • onRestoreInstance:被回收后恢复时调用
    • onConfigurationChanged:配置更改后调用

    常规流程:略。
    注意:

    • 当新Activity的theme属性android:windowIsTransparent为true时,无论视觉上是否会遮挡当前的Activity,当前Activity都被视为可见,即onStop方法不会被调用。新Activity销毁返回时,onStart和onRestart方法也不会被调用。如果在新Activity中出现横竖屏切换操作,下面的Activity也会执行横竖屏切换。因为系统认为下面的Activity是“可见的”。

  • Activity之间的通信方式

    • 打开新Activity时利用Intent携带数据
    • 新Activity销毁时setResult返回数据
    • 广播Broadcast
    • 事件总线
    • 文件...

  • fragment各种情况下的生命周期

    • onAttach:当Fragment与Activity相关联时调用
    • onCreate:当Fragment被创建时调用
    • onCreateView:当Fragment的布局被创建时调用
    • onViewCreated:当Fragment的布局被创建完毕时调用
    • onActivityCreated:当Fragment所关联的Activity已经创建完毕,即Activity的onCreate()已经执行完毕时,会调用
    • onStart:当Fragment可见时,会调用;此时Fragment不可响应交互
    • onResume:当Fragment可见并且Activity处于运行时状态时会调用;此时Fragment可以响应交互
    • onPause:当Fragment不可响应交互时会调用
    • onStop:当Fragment不再可见时会调用
    • onDestroyView:当onCreateView()中所创建并返回的View已经从Fragment中分离出来时会调用
    • onDestory:当Fragment不再被需要时会调用
    • onDetach:当Fragment与Activity解除关联时会调用

    注意1:在事务中是否添加addToBackStack()是会影响Fragment的生命周期的

    • 不加addToBackStack():
      • 当Fragment被替换时,会依次回调 onPause()-onStop()-onDestroyView()-onDestory()-onDetach()
      • 当用户按返回键时,会直接退出当前Activity
    • 加了addToBackStack():
      • 则会将事务添加到回退栈中
      • 当Fragment被替换时,会依次回调 onPause()-onStop()-onDestroyView(),不会调用 onDestory()-onDetach()
      • 当用户按返回键时,会回滚该事务,依次调用 onCreateView()-onViewCreated()-onActivityCreated()-onStart()-onResume()

    注意2:FragmentTransaction

    • add():onAttach()-onCreate()-onCreateView()-onViewCreated()-onActivityCreated()-onStart()-onResume()
    • remove():onPause()-onStop()-onDestroyView()-onDestory()-onDetach()
    • replace():相当于旧的Fragment执行了remove(),新的Fragment执行了add()
    • show()/hide():不会回调任何生命周期函数,因为调用该方法的前提是该Fragment已经被创建并添加到容器中,只是对Fragment进行显示/隐藏
    • detach():onPause()-onStop()-onDestroyView()
    • attach():onCreateView()-onViewCreated()-onActivityCreated()-onStart()-onResume()

  • Fragment之间的通信方式

    • 通过Activity的FragmentManager获取到对应的Fragment实例直接通信
    • 通过接口方式
    • 事件总线
    • google的新组件ViewModel
    • ...

  • Activity 怎么和Service 绑定

    1. 自定义一个类继承Service;
    2. 自定义一个类继承Binder;
    3. 在Service的onBind方法里返回自定义的Binder实例;
    4. 在Activity中调用bindService方法,绑定成功后会把onBind返回的Binder实例回调到ServiceConnection的onServiceConnected方法上。到这里就绑定成功。

  • Service各种情况下的生命周期

  • Servce与Activity的通信方式

    • 通过bindService方式启动服务,可以从ServiceConnection里面拿到Service提供的Binder对象进行通信;
    • 通过广播
    • 事件总线
    • 等等...

  • 谈谈你对ContentProvider的理解

  • 广播使用的方式和场景

  • Activity-Window-View区别

    Activity是四大组件之一,负责提供展示和交互,但它自身并不具备这些功能,而是通过Window来提供。Window在Android表示一个窗口,但它也是一个抽象的概念,实际提供显示的组件是它内部的DecorView,WindowManager通过IPC调用WindowManagerService来对Window进行管理。

    当Activity调用setContentView时,实际是调用了自身的Window实例PhoneWindow的setContentView方法。方法里会初始化DecorView,然后把Activity::setContentView方法的布局加载到DecorView里面一个id为content的父布局中,这样就完成了它们之间的绑定。

  • DecorView结构

    DecorView是一个FrameLayout,包含了一个LinearLayout和一个状态栏View。其中LinearLayout主要放的是id为content的布局,也就是我们的布局的父布局。如果有使用系统的ActionBar的话,LinearLayout里面还会有ActionBar的布局,在content上面。总体布局大概如下:

    • DecorView
      • LinearLayout
        • action_bar_container
        • content
      • StatusBar

  • 如何优化自定义View

    • 减少重绘请求和重布局请求
    • 利用硬件加速
    • 不在onDraw等回调方法里面创建对象
    • 利用Canvas.clipRect方法避免过度绘制

  • 低版本SDK如何实现高版本api

    1. 用@TargeApi($API_LEVEL) 使可以编译通过, 不建议使用SuppressLint"NewApi");
    2. 运行时判断API level; 仅在足够高,有此方法的API level系统中,调用此方法;
    3. 保证功能完整性,保证低API版本通过其他方法提供功能实现。

  • Binder原理

    Binder通信是Android系统进程间通信方式之一。相对于从Linux内核里面继承的其他IPC方式,例如管道、共享内存等,具有更高的安全性和传输性能。

    Binder框架是基于CS架构的,它定义了四个角色:

    1. Server:
      服务端,提供实际方法调用的进程。它持有的是Binder本地对象,也是远程方法调用的实际执行者。
    2. Client:
      客户端,或者说调用端,发起远程方法调用的进程。它持有的是Binder代理对象,它是Binder本地对象的一个远程代理,由它发出的方法调用请求,实际是通过Binder驱动最终转发到服务端的Binder本地对象执行返回结果。
    3. Binder驱动:
      主要负责提供进程之间Binder的连接、通信等底层支持。
    4. ServiceManager:
      类似于网络通信中的路由,提供根据名称来注册、查询和返回代理Binder对象的功能。

    一次完整的Binder调用过程是这样的:

    1. Server启动时向ServiceManager注册其名字的Binder服务;
    2. Client向ServiceManager查询该服务;
    3. ServiceManager通过Binder驱动返回Server的Binder的代理对象;
    4. Client拿到代理对象就可以通过Binder驱动进行进程间通信。
    5. Client最终的请求是通过代理对象的transact方法发出,通过Binder驱动转发到Server的Binder本地对象的onTransact方法中执行得到结果。

    在实际的Binder使用中,我们大多数情况下是使用AIDL来完成的。我们只需要实现好Server端的Binder接口,就可以通过bindService方法在Client拿到相同接口代理对象,剩下的就像操作本地对象一样进行方法调用即可。

  • AIDL原理

    Android中的大部分IPC都是通过Binder完成的。AIDL可以看作是对Binder使用的一种封装工具。当我们不使用AIDL直接使用Binder进行通信时,每一个client的请求都要自己创建并写入请求序列化参数,创建输出对象,调用transact方法,然后在server端的Binder自己实现onTransact方法,根据输入的方法code一个一个的处理对应的方法和参数。而AIDL就是把这些麻烦的过程,封装成跟普通方法调用一样的外观。

    使用Android Studio创建X.aidl文件后,会自动生成一个对应的X.java文件。文件中包含一个接口和2个内部类:

    • 接口X:继承自接口IInterface,表示这是Binder对象的方法接口。接口X有在aidl文件中定义的方法。
    • 静态抽象内部类X.Stub:继承自Binder,并实现接口X。Stub是Binder框架中服务端的Binder本地对象的基类,它为接口X中自定义方法添加标记,并且实现了Binder的onTransact方法,根据方法的标记对远程请求一一处理好。使用中需要在Server端服务的onBind方法中返回此类的实现对象。
    • 静态内部类X.Stub.Proxy:实现接口X,并持有IBinder对象mRemote。它是Binder框架中客户端调用的封装。在使用时,X.Stub.Proxy对象是通过X.Stub::asInterface方法创建。该方法需要一个IBinder参数,在客户端调用时,实际上这个IBinder对象就是向ServiceManager查到的服务端的Binder本地对象的远程代理。Proxy在初始化时把这个远程对象赋值给了mRemote。接口X的方法实现中,正是调用了mRemote::transact方法进行远程调用,从而实现对方法调用的封装。

  • IntentService原理及作用是什么?

    Service是运行在主线程的,所以不能进行耗时操作,否则可能会导致界面不流畅,甚至有ANR的风险。但实际开发中确实有这样的需要,这时我们会考虑到在Service中使用线程去处理耗时操作,而IntentService就是一个Service+Thread的封装。

    IntentSevice在服务创建方法onCreate调用时就启动一个HandlerThread线程,并把线程中创建的Looper通过构造函数给ServiceHandler,所以通过该ServiceHandler发出的消息都会在此线程中处理。IntentService的onStart方法会把启动Service的Intent赋值给message.obj,然后使用ServiceHandler把消息发送到子线程来进行处理。在ServiceHandler的handleMessage方法中,会首先执行IntentService的抽象方法onHandleIntent(实际工作内容),并在之后调用stopSelf。所以,当handleMessage方法接收到消息并进行处理后,会第一时间停止服务,并在onDestory方法里调用Looper::quit来停止子线程的Looper,关闭线程。

    所以使用中完全不用考虑线程和服务的回收等问题,继承并实现onHandleIntent方法,在方法中编写自己的耗时任务代码即可。

  • RecyclerView原理

  • 属性动画原理

  • 多线程在Android中的使用

    1. 直接使用Thread
    2. AsyncTask(弃用)
    3. HandlerThread
    4. ThreadPoolExecutor
    5. IntentService

  • 进程的生命周期

  • Handler机制

    Handler机制涉及几个角色:

    • Handler:
      主要作用是发送消息和处理消息。发送消息可以通过一系列post方法发送一个Runnable,也可以通过sendMessage系列方法发送一个Message,最终都是调用sendMessageAtTime方法,把封装好的Message放入消息队列中。dispatchMessage方法接收到Looper的消息,如果是Runnable消息就直接执行,否则调用handleMessage方法进行处理。

    • Message:
      消息对象类。类中包含一个全局静态的消息对象池以便复用。当使用obtain方式获取Message实例时,实际上是优先从对象池中拿取。当Looper处理完一个消息时,也会回收消息到此对象池中直至对象池满。对象池是链表结构。Message拥有一个指向本消息处理者,也是本消息的发送者对象的引用mTarget,Looper拿到消息时,会从mTarget字段拿到消息的处理者进行分发。

    • MessageQueue:
      消息队列,数据结构是单向链表。Handler发出的消息会存放到此,而Looper会不断循环从队列中拿出消息。Handler调用sendMessageAtTime,实际上是调用了MessageQueue对象的enqueueMessage方法,根据消息的响应事件从近到远的顺序插入到队列中。next方法可以从队列中取出需要执行的消息对象,它可能会阻塞线程:

      • 当下一个需要处理的消息的时间还没到的时候,会阻塞,阻塞的时间就是离下一个消息要被处理的时长。
      • 当队列中没有消息的时候,无限阻塞。

      进入阻塞的状态时,mBlock标记会变为true,当其他线程通过Handler调用了MessageQueue的enqueueMessage方法往队列中添加消息时,如果mBlock标记为true,就会唤醒主线程,从next方法被阻塞的位置开始继续循环查找可返回的Message。

      当Looper调用quit方法退出循环时,实际调用的是MessageQueue的quit方法。quit方法会把队列消息清空,并且把mQuitting标记为true,并且唤醒主线程。next方法看到mQuitting标记,就会返回null。

    • Looper:
      消息分发器,每个线程最多只能有一个,是Handler机制的核心。调用Looper.prepare会创建一个Looper实例,并把此实例存放到静态的ThreadLocal中,所以确保了每个线程都只能有一个Looper。每个Looper拥有自己的一个消息队列MessageQueue。loop方法会在当前线程开始一个无限循环,循环中会调用自己的MessageQueue的next方法取出需要处理的消息,然后调用其target.dispatchMessage方法进行分发处理。当next方法拿到null对象时,表示调用了quit方法要停止Looper,所以loop会退出循环。

    总结,Handler可以拥有多个,可以在不同线程被调用;MessageQueue的enqueue方法会运行在生产线程,next方法是Looper调用的,所以是运行在Looper线程。每个线程最多只有一个消息队列;Looper运行在自己的线程,每个线程最多拥有一个Looper。Handler发送消息到消息队列,Looper不断从当前线程循环拿出队列中的消息,并且根据消息的target来拿到处理的Handler对象,把消息分发到对象的dispatchMessage方法上,这就完成了一次消息的生产-消费。

    子线程的Handler要发送消息之前,必须先创建本线程的Looper并且调用loop方法。而UI线程不用的原因是,在ActivityThread创建时,就已经为主线程创建了Looper。

  • Activity启动流程

  • Gradle Build过程

    1. Preparation of dependecies 在这个阶段gradle检测module依赖的所有library是否ready。如果这个module依赖另一个module,则另一个module也要被编译。
    2. Merging resources and processing Manifest 打包资源和Manifest文件
    3. Compiling 在这个阶段处理编译器的注解,源码被编译称为class文件
    4. PostProcessing
    5. ReleaseAndPackage

  • 插件化原理

    插件化的核心功能(简化为Activity的插件化):打开一个非本应用自带的Activity,并正常运行。

    在实现这个功能的过程中,会遇到几个主要的问题:

    1. 怎么加载外部的 Activity 类?
    2. 外部的 Activity 类没有在AndroidManifest中注册,怎么绕过?
    3. 上面问题都解决后,发现资源加载的不对,怎么办?

    以上三个问题,下面逐一解答。