Java
Java基础知识
1. 什么是面向过程?什么是面向对象?(OOP)
- 面向过程:面向过程讲究的是步骤化,分析要实现的需求的所有步骤,并通过函数(方法)一步一步地实现这些步骤,然后依次调用即可。
- 面向对象:面向对象讲究的是行为化,把要实现的需求按照特点、功能划分,将这些存在共性的部分封装成类,类经过实例化后才是对象,而创建对象不是为了完成某个步骤,而是为了描述某个事物在解决问题时的步骤的行为。
2. 面向过程跟面向对象的优缺点?
- 面向过程
1)优点:性能上是优于面向对象的,因为类在调用的时候需要实例化,开销过大。
2)缺点:不易维护、复用、扩展
- 面向对象
1)易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护
2)一般来说性能比面向过程低(视情况而定)
3. 面向对象的四大特性
- 封装:将数据和行为组合在一个类中,并对对象的使用者隐藏具体的实现方式,然后提供公共的访问方式供对象进行访问。
- 继承:当多个类具体共同的数据和行为时,把这些共同的数据(成员变量)和行为(成员方法)抽取到一个类中,也就是父类,然后由这些类去继承父类,从而获取得到父类的公共属性,并可以扩展自己的特有属性。
- 多态:多态的前提是有子父类继承关系,多态就是指同一种事物在不同的情况下的多种表现形式,简单来说就是父类引用变量指向子类对象,格式为:父类类型 变量名 = new 子类类型();
- 抽象:就是将一类事物的共同特性抽取出来封装在一个抽象类中,由抽象类或接口来体现。
4. 面向对象的六大原则
- 单一职责原则:一个类只应该负责一种职责,术语叫:仅有一个引起其变化的原因。
- 开闭原则:一个类应当对扩展开放,对修改关闭。
- 接口隔离原则:使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。
- 依赖倒置原则:抽象不应该依赖于细节,细节应该依赖于抽象。换言之:针对接口编程,而不是针对实现编程。
- 里氏替换原则:所有引用基类的地方必须能透明的使用其子类的对象。依赖于面向对象的继承与多态特性。
- 迪米特法则(最少知道原则):一个类应该尽可能少的与其他类发生相互作用。换言之:一个类应该对自己需要调用的类知道得最少,类的内部如何实现、如何复杂都与调用者或者依赖者没关系,调用者或者依赖者只需要知道它需要的方法即可。
5. 类之间的关系
- 依赖:如果一个类的方法使用或操纵另一个类的对象,我们就说一个类依赖于另一个类;
- 聚合:一个类的对象包含另一个类的对象,例如Class对象包含着Student对象,也就是一个类的对象由其他类的对象组合而成;
- 继承:类A扩展类B,类A不但包含从类B继承的方法,还会有一些额外的功能。
6. 8种数据类型
1. int:4字节,取值范围为 -2^31 ~ 2^31-1 (-2147483648 ~ 2147483647);
2. short:2字节,取值范围为 -2^15 ~ 2^15-1 (-32768 ~ 32767);
3. long:8字节,取值范围为 -2^63 ~ 2^63-1 (-9223372036854775808 ~ 9223372036854775807);
4. byte:1字节,取值范围为 -2^7 ~ 2^7-1 (-128 ~ 127);
5. float:4字节,6~7位有效数字。当一个浮点数后面没有F或f后缀,会默认为double类型;
6. double:8字节,15位有效数字;
7. char:字符型
8. boolean:布尔值
7. 泛型程序设计以及泛型擦除
- 为什么要使用泛型程序设计?
泛型程序设计意味着编写的代码可以对多种不同类型的对象重用,最常用的例子就是ArrayList,如果没有使用泛型设计的话,那么在使用ArrayList的时候无法根据具体的类型参数去传入数据,很有可能会造成运行时出现类的强制类型转换异常。
- 泛型程序的编写
1)泛型类就是有一个或多个类型变量的类
2)泛型方法可以在普通类中定义,也可以在泛型类中定义。注意:类型变量放在修饰符的后面,并在返回类型的前面
3)类型变量的限定,一个类型变量或通配符可以有多个限定,如:<T extends Comparable & Serializeble>
- 泛型擦除
1)概念:无论何时定义一个泛型类型,都会自动提供一个相应的原始类型,这个原始类型的名字就是去掉类型参数后的泛型类型名,即类型变量会被擦除,并替换为其限定类型,对于没有限定类型的变量,则替换为Object。
2)转换泛型表达式:编写一个泛型方法时,如果擦除了返回类型,编译器会插入强制类型转换。
3)转换泛型方法:会合成桥方法来保持多态。
- 场景:当一个父类定义了泛型类型,而当子类想要去继承父类并重写父类中的方法时,由于虚拟机的泛型擦除问题,会把父类的泛型类型擦除为原始类型Object,而子类重写的方法原意是想要将父类的泛型类型指定为子类,由于泛型擦除问题,导致重写变成了重载,故无法实现多态;
- 解决方法:在编译过程中编译器会自动生成桥方法,桥方法的参数类型都是经过擦除后的原始类型,而桥方法的内部实现就是调用我们自己重写的那两个方法,解决了泛型擦除和多态的冲突。
- 注意:当两个方法具有相同的参数类型是不合法的,因为方法签名是一样的,也就是说虚拟机根本不能分别这两个方法。如果是我们自己编写的Java代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,因为虚拟机通过参数类型和返回类型来确定一个方法,所以编译器为了实现泛型的多态允许自己做这个看起来“不合法”的事情,然后交给虚拟器去区别。
- 通配符类型
8. 序列化
- 概念:把对象转化成可传输的字节序列过程称为序列化。
- Serializable:使用了Serializable接口进行序列化与反序列化的对象,虽然恢复后的对象与原对象的内容一致,但是并不是同一个对象。在实现Serializable接口时需要声明一serialVersionUID,这个字段可以手动赋值也可以由系统根据当前类的结构自动去生成它的hash值,只有序列化与反序列化时两者的serialVersionUID值相同,才可以正常进行反序列化。**注:如果是通过系统自动生成的hash值,有可能会造成反序列化时的类结构有所改变,生成的hash值不一致了,从而导致反序列化失败。静态属性与transient关键字标记的成员变量不参与序列化过程**。
- Parcelable:
- 两者的区别:Serializable是Java中的序列化接口,其使用起来简单但是开销很大,Serializable的本质是使用了反射,在序列化过程中需要大量的I/O操作,会产生很多临时变量,造成频繁的GC,优点是持久化;而Parcelable是Android中的序列化方式,缺点是使用繁琐,但是效率高,在外界有变化的情况下,不能很好的保证数据的持久性,Parcelable的本质是将一个完整的对象进行分解,而分解后的每一部分都是Intent所支持的类型。
- 如何选择:Parcelable主要用于内存传输,而Serializable主要用于网络传输或存储设备传输。
8. Java的几种集合
- Map集合:常用的有HashMap、TreeMap、HashTable、LinkedHashMap
- Set集合:继承自Collection接口,常用的有HashSet、TreeSet
- List集合:继承自Collection接口,常用的有ArrayList、LinkedList
9. String、StringBuffer、StringBuilder的区别和使用场景
- String:使用final修饰的,数据存在常量池中,当需要修改值的时候相当于重新进行了new String操作,由于是常量,所以是线程安全的,但是执行速度是最差的,一般用于少量字符串操作的时候。
- StringBuffer:是线程安全的,内部是通过char数组实现的,执行速度比String高,但是比StringBuilder低,因为内部考虑到了多线程的情况。
- StringBuilder:是非线程安全的,内部也是通过char数组实现的,执行速度比其他两个高,但是不适用于多线程的情况。
10. Object的8个方法
- equals:判断对象是否相同
- hashCode:同equals结合使用,通过重写该方法,然后判断两个对象是否相同
- wait:使线程处于等待状态并释放持有的锁
- notify:唤醒线程,然后去重新竞争锁,只能唤醒一个线程
- notifyAll:唤醒所有处于等待队列的锁
- finalize:释放资源,用于垃圾回收
- clone:只有实现了 Cloneable 接口才可以调用该方法,属于浅拷贝
- toString:
11. 深拷贝、浅拷贝的区别?以及Object中的clone是哪种拷贝?
- 深拷贝:深拷贝是创建一个一模一样的对象,跟原对象只有值一样,而引用不一样,即不共享同一块内存;
- 浅拷贝:浅拷贝复制的是原对象的引用,当修改新对象的时候原对象也会被改变,即共享同一块内存;
- Object中的clone是浅拷贝。
Java虚拟机
1. 内存模型图,基本工作流程
2. JVM内存区域划分,每块区域做什么的
- 堆:主要存放对象,分为新生代跟老年代,其中新生代又分为一个Eden区跟两个Survivor区,比例是8:1:1,新生代使用的是标记-复制算法,老年代使用的是标记-整理算法;
- 方法区:主要存放类的信息、静态属性、常量池;
- 虚拟机栈:局部变量表、操作数栈、动态链接、方法返回出口;
- 本地方法栈:
- 程序计数器:
3. 垃圾回收机制
5. 垃圾回收算法
- 标记-清除算法:首先标记出所有需要回收的对象,在标记完成后,统一回收所有被标记的对象,也可以反过来操作。缺点是执行效率不稳定、容易产生大量内存碎片。
- 标记-复制算法:把内存分为大小相等的两块,每次只使用其中一块,当这一块内存用完了,就把还存活的对象复制到另一块内存中。缺点是浪费空间,可用内存缩小为原来的一半。
- 标记-整理算法:
- 分代收集算法:
5. Dalvik、ART虚拟机
7. GC Roots有哪些?
- 在虚拟机栈中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等;
- 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量;
- 在方法区中常量引用的对象,譬如字符串常量池里的引用;
- 在本地方法栈中JNI引用的对象;
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(如NullPointException、OutOfMemoryError)等,还有系统类加载器;
- 所有被同步锁持有的对象;
- 反映虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
7. 类加载机制
- 概念:Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。
- 生命周期如下:其中验证、准备、解析三个部分统称为连接。
- 加载:
- 验证:
- 准备:
- 解析:
- 初始化:
- 使用:
- 卸载:
- 触发初始化的的六个条件:
1)遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化阶段,而能够生成这四条指令的典型Java代码场景有:
- 使用new关键字实例化对象的时候;
- 读取或设置一个类的静态字段的时候(被final修饰、已在编译期把结果放入常量池的静态字段除外);
- 调用一个类的静态方法的时候;
2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类还没有进行过初始化,则需要先触发其初始化;
3)当初始化类的时候,如果发现其父类还没有被初始化,则需要先触发其父类的初始化(接口与类不同,接口只有在真正使用到父类的时候才会初始化);
4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main( )方法的那个类),虚拟机会先初始化这个主类;
5)当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getstatic、REF_putstatic等四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化;
6)当一个接口中定义了JDK 8新加入的默认方法(被default修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
- 典型的被动引用的三个例子:
1)通过子类调用父类的非final静态字段,不会导致子类的初始化,只会导致父类的初始化;
2)通过数组定义来引用类,不会触发此类的初始化,而是会触发另一个由编译器自动生成的类;
3)调用一个类的final型静态字段,不会触发该类的初始化,因为常量位于常量池中。
Java多线程
1. synchronized、volatile
2. 线程sleep、join、yield、wait、notify
3. Lock接口
- ReentrantLock:
- ReentrantReadWriteLock:
4. 常用的原子类
5. 线程池的种类以及区别
1)FixThreadPool:只有核心线程,当线程空闲时,不会被回收,等待的队列不限制大小。
2)SingleThreadPool:只有一个核心线程,确保所有任务都在同一线程中按顺序完成,因此不需要处理线程同步的问题。可以理解为FixThreadPool的参数被手动设为1的场景。
3)CachedThreadPool:只有非核心线程,最大线程数没有限制,空闲时间为60s。
4)ScheduledThreadPool:核心线程数固定,非核心线程数没有限制,支持延迟执行和周期重复执行。
6. 构造线程池的必要参数以及作用
线程池的作用:使用线程池能够减少创建和销毁线程的性能开销,还能控制线程池中的并发数。
1)线程池的核心线程数量;
2)线程池的最大线程数量;
3)非核心线程空闲时最多能存活多长时间;
4)等待执行任务的队列;
5)拒绝策略,当提交的任务过多而不能及时处理时,需要定制策略来进行处理.
7. 什么是死锁?如何预防死锁?
- 死锁概念:当两个或两个以上的线程在执行过程中,由于在互相等待根本不可能被释放的锁,从而导致所有的任务无法继续完成,造成线程”假死“,称为死锁;
- 如何检测:可以使用JDK自带的工具来检测是否有死锁的现象,首先进入JDK的bin目录下,通过执行jps、jstack命令来检测,并且可以看到是哪个线程的第几行代码造成的死锁现象;
- 造成死锁的四个条件:
1)互斥条件:一个资源每次只能被一个线程使用
2)请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放
3)不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺
4)循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系
- 如何解决:只要破坏上述四个条件之中的任何一个条件,都能避免死锁的发生。由于互斥条件是是不能被破坏的,所以不考虑破坏这个条件。
1)破坏请求保持条件:synchronized在使用过程中如果无法获取得到锁,则会一直尝试获取锁,所以可以使用Lock的tryLock()方法来限定时间,当超过一定的时间而无法获取得到锁的时候,就释放自己原本得到的资源;
2)破坏循环等待条件:按照顺序申请资源。
Java引用
1. Java的四种引用及其作用
1)强引用:默认创建的对象都是强引用,当一个对象是强引用时,不管内存是否不足,都不会被回收
2)软引用:当内存不足的时候才会被回收
3)弱引用:不管内存是否不足,只要进行了GC,就会被回收
4)虚引用:随时可能被回收。它的作用在于跟踪垃圾回收的过程,虚引用必须要与引用队列一起使用,当虚引用引用的对象被垃圾回收器回收了,就会收到一个系统通知。
2. LeakCanary的原理
设计模式
1. 单例模式
2. 建造者模式
3. 策略模式
4. 观察者模式
5. 简单工厂模式
6. 适配器模式
7. 装饰模式
Kotlin
kotlin基础知识
1. Kotlin的标准函数run、let、with、apply、also的区别?
- let:let函数可以通过it参数去使用调用let函数的对象。
- run:run函数与with函数类似,区别在于with函数可以直接调用,而run函数必须要通过某个对象才可以调用,也就是xxx.run{},相当于把with函数的第一个参数移到run函数前面了,并且run函数只接收一个lambda表达式,表达式中的内容与with函数一样,也是使用最后一行代码作为返回值。
- with:with函数接收两个参数,第一个可以是任意一个类型的对象,第二个是一个Lambda表达式。with函数会在Lambda表达式中提供第一个参数的上下文,即可以直接在表达式中直接调用这个参数的所有方法,并使用最后一行代码作为返回值。
- apply:apply函数与run函数类似,都是要通过某个对象才可以调用,并且只接收一个lambda表达式,也会在表达式中提供对象的上下文,区别在于不能指定返回值,而是会自动返回调用对象本身。
- also:可用于不更改对象的其他操作,比如在链式编程中插入一句打印日志的代码。
2. 密封类是什么?有什么作用?
密封类是使用sealed class修饰的,它可以被继承。当在when()中传入一个密封类变量作为条件时,Kotlin编译器会自动检查该密封类有哪些子类,并强制要求将每一个子类所对应的条件全部处理。
注:密封类及其所有子类只能定义在同一个文件的顶层位置,不能嵌套在其他类中。
3. 什么是高阶函数?
- 概念:如果一个函数接收另一个函数作为函数,或者返回值的类型是另一个函数,那么该函数就称为高阶函数。
- 出现的问题:Kotlin的编译器会将高阶函数的语法转换成Java支持的语法结构,即函数参数变成了一个Function接口,里面有一个待实现的invoke()方法,由于这个接口是匿名内部类,所以会造成额外的内存和性能开销。为了解决这个问题,Kotlin提供了内联函数的功能。
- 内联函数:Kotlin编译器会将内联函数中的代码在编译的时候自动替换到调用它的地方。
4. 泛型和委托
5. 协程
6. 扩展函数
Kotlin面试题
1. kotlin的扩展函数相当于java的哪种方法?
2. kotlin的扩展函数内部实现原理
Android
Android组件
1. Activity启动模式及使用场景
2. Activity的生命周期,正常情况 / 异常情况下
- 正常情况:
- 异常情况:在异常情况下(如Activity横竖屏切换),Activity会被销毁并重新创建,其onPause、onStop、onDestroy均会被调用,同时会调用onSaveInstanceState来保存当前Activity的状态,**这个方法的调用时机在onStop之前,与onPause没有既定的时序关系,既可能在onPause之前调用,也可能在onPause之后调用**。当Activity被重新创建之后,系统会调用onRestoreInstanceState,并且把销毁时onSaveInstanceState保存的Bundle状态对象作为参数**同时**传递给onRestoreInstanceState和onCreate方法,onRestoreInstanceState的调用时机在onStart后。
3. Activity的四种状态
- Running(运行):在屏幕前台(位于当前任务栈栈顶)
- Paused(暂停):失去焦点但仍然对用户可见(覆盖的Activity可能是透明的或者未完全遮挡)
- Stopped(停止):完全被另一个Activity遮挡
- Destroyed(销毁):Activity已退出,已完全销毁
4. Activity的启动流程
5. Service的两种启动方式
1)startService:使用该方法启动的Service会在后台运行,其生命周期跟启动它的context没有任何关系,也不能与Context进行通信。生命周期为onCreate()->onStartCommand()->onDestory()。
2)bindService:使用该方法启动的Service会与启动它的context进行绑定,在context中解绑之后,该Service也就结束了,它可以与context进行通讯,在绑定的时候通过onBind()回调,在这个方法中获取Service传递过来的IBinder对象,通过这个对象实现跟宿主交互。生命周期为:onCreate()->onBind()->onUnbind()->onDestroy()。
6. IntentService
IntentService是继承自Service的,它的优点在于能处理异步请求,实现多线程,并且在任务完成后会自动停止线程。它在onCreate()方法中通过HandlerThread来创建新线程,然后通过重写onHandleIntent()方法来执行任务。
7. BroadcastReceiver注册
- 分别是静态注册和动态注册两种方式。
8. BroadcastReceiver有哪几种类型
- 普通广播 系统广播 有序广播 粘性广播
9. ContentProvider
10. Intent能传递哪些数据类型
- 8中基本数据类型(int、short、long、float、double、char、byte、boolean)
- String、StringBuilder、StringBuffer(都实现了Serializable接口)
- CharSequence
- 实现了Serializable接口或Parcelable接口
11. Notification在8.0之后的变更
12. App的启动流程
- 重要的系统成员如下:
1)init进程:当Android系统启动的时候,linux的根进程init进程是启动的第一个进程,然后init进程才会启动Zygote进程;
2)Zygote进程:是所有进程的父进程,负责应用进程的创建;
3)SystemServer:系统服务进程,负责系统中大大小小的事物,也启动了3名大将,分别是:ActivityManagerService、PackageManagerService、WindowManagerService以及Binder线程池;
4)ActivityManagerService:主要负责系统中四大组件的启动、切换、调度以及应用进程的管理和调度,对于一些进程的启动,Launcher会通过Binder通信机制传递给AMS,并由AMS通过socket传递给Zygote;
5)PackageManagerService:主要负责应用包的一些操作,如安装、卸载、解析AndroidManifest.xml等;
6)WindowManagerService:主要负责窗口相关的一些服务,比如窗口的启动,添加,删除等;
7)Launcher:桌面应用,一般开机会自启动,通过设置Intent.CATEGORY_HOME的Category隐式启动。
- 启动流程如下:
1)在用户点击Launcher桌面图标时,首先会调用Instrumentation对象的execStartActivity()方法,由于Launcher单独位于一个进程,所以需要通过跨进程通信与ATMS进行通信,告诉它需要启动App; 注:原来的通信工作都是属于AMS的,现在分了一部分给ATMS,主要包括四大组件的调度工作,也是由SystemServer进程直接启动的。
2)ATMS在收到消息时,就会让上一个应用(也就是Launcher)进入Paused状态,然后判断上一个应用是否已经启动,如果已经启动,则会调用ResumeActivityItem方法,这个方法是用来控制Activity的onResume()以及onStart()方法的;
3)当Launcher进入Paused状态时,ATMS会通过WindowProcessController判断要打开的应用进程是否已经启动,当一个应用进程已经启动的时候,会在ATMS里保存一个WindowProcessController信息,包括processName和uid,若进程已经启动,则直接启动Activity即可,若没有启动,则通过socket与Zygote进行通信,告诉Zygote要fork一个进程,并返回新进程的pid;**注:使用socket而不使用Binder的原因是因为fork不允许存在多线程,而Binder通信是多线程的**。
4)在Zygote进行fork进程的过程中,会通过反射调用ActivityThread的main方法,也就是应用程序的入口,在main方法中会创建ActivityThread以及Looper对象,并开启Looper.loop()循环;
5)进程创建完之后,会分别调用bindApplication(启动Application)、makeActive(设定WindowProcessController里面的线程,判断进程是否存在的时候需要用到)、attachApplication(启动根Activity)方法;
6)创建Application:首先会创建Instrumentation,每个应用程序都会有一个Instrumentation,用于管理这个进程,比如要创建Activity时,需要调用Instrumentation的方法;然后调用attach方法,最后调用onCreate方法;
7)启动Activity:先创建上下文对象(ContextImpl),然后通过类加载器加载Activity,最后调用Acticity的onCreate()方法。
- 可优化的地方:
1)Application的attach方法,MultiDexApplication会在方法里面会去执行MultiDex逻辑。所以这里可以进行MultiDex优化,比如今日头条方案就是单独启动一个进程的activity去加载MultiDex。
2)Application的onCreate方法,大量三方库的初始化都在这里进行,所以我们可以开启线程池,懒加载等等。
3)Activity的onCreate方法,同样进行线程处理,懒加载。或者预创建Activity,提前类加载等等。
性能优化
1. 布局优化
2. 内存优化
3. 启动优化
4. 卡顿优化
5. apk体积优化
6. 数据结构优化
跨进程通信的方式(IPC)
1. Bundle
2. AIDL
3. Binder
4. 文件共享
5. Messenger
6. ContentProvider
7. Socket
跨线程通信方式
1. Handler
2. AsyncTask
3. HandleThread
4. IntentService
5. RxJava
Android View
1. View、ViewGroup的绘制流程
1)View的绘制流程开始于ViewRootImpl对象的performTraversals(),performTraversals()方法里面分别调用了performMeasure()、performLayout()、performDraw()。View的绘制是从顶级的DecorView的ViewGroup开始,一层一层从ViewGroup至子View遍历测量绘制,即自上而下遍历、由父视图到子视图、每一个ViewGroup负责测绘它所有的子视图,而最底层的View会负责测绘自身。
2)Measure过程:
- ViewGroup测量过程如下:
(1)调用performMeasure()
(2)调用measure()进行基本测量逻辑的判断、调用onMeasure()进行下一步的测量
(3)重写onMeasure(),遍历所有的子View以及测量measureChildren()、合并所有子View的尺寸并计算出ViewGroup的尺寸、存储测量后的子View宽高。
(4)measureChildren(),遍历子View,并调用measureChild进行子View的下一步测量
(5)getChildMeasureSpec(),根据父View的MeasureSpec参数和子View的布局参数来计算子View的MeasureSpec参数
(6)setMeasureDimension(),存储测量后的子View宽高
- View测量过程如下:
(1)调用measure()进行基本测量逻辑的判断、调用onMeasure()进行下一步的测量
(2)调用onMeasure(),根据自身的宽高的测量规格计算自身的宽高值getDefaultSize()
(3)getDefaultSize(),根据子View宽高的测量规格计算子View的宽高值
(4)setMeasureDimension(),存储测量后的子View宽高
3)Layout过程:
- ViewGroup布局过程如下:
(1)调用layout():完成父View的位置布局(调用setFrame())
(2)重写onLayout():遍历子View,计算子View的位置,最终确定所有子View在父View中的位置,即layout过程完毕
- View布局过程如下:
(1)调用layout():计算自身的位置(调用setFrame())
(2)onLayout():这是一个空实现
4)Draw过程:
- ViewGroup绘制过程如下:
(1)调用draw():绘制自身,在这个方法内调用了drawBackground()来绘制自身背景,并且调用了onDraw()来绘制自身内容,还调用了dispatchDraw()来绘制子View
(2)重写onDraw():绘制自身需要的内容
(3)dispatchDraw():绘制子View
(4)onDrawScrollBars():绘制装饰,如滚动条
- View绘制过程如下:
(1)调用draw():绘制自身,在这个方法内调用了drawBackground()来绘制自身背景
(2)重写onDraw():绘制自身需要的内容
(3)dispatchDraw():空实现
2. 自定义View的onDraw( )方法需要注意什么?
需要注意不要在onDraw方法里new对象,因为onDraw会调用多次,如果在里面new对象的话,容易造成内存抖动。
3. requestLayout( )和invalidate( )区别
1)调用invalidate()方法不会导致measure和layout方法被调用,只会调用draw方法(只能在UI线程使用)
2)调用requestLayout()方法,会导致布局重绘,调用measure、layout、draw的过程。
4. View的事件分发机制
5. 滑动冲突
- 常见的滑动冲突场景分为三种
1)外部滑动方向与内部滑动方向不一致:当外部View是左右滑动的,内部View是上下滑动的,可以根据滑动方向来解决滑动冲突,即当用户左右滑动时,让外部View拦截事件,当用户上下滑动时,让内部View拦截事件。
2)外部滑动方向与内部滑动方向一致:滑动方向一致的时候,无法通过滑动方向去解决冲突,但是可以根据具体规则去处理,
3) 上面两种情况的嵌套
- 滑动冲突的解决方法有两种
1)外部拦截法:点击事件都要经过父容器,若父容器需要此事件就拦截,不需要就不拦截,需要重写父容器的onInterceptTouchEvent()方法。
2)内部拦截法:指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就消耗掉,否则就交由父容器进行处理。内部拦截法需要配合requestDisallowInterceptTouchEvent去实现,也就是重写dispatchTouchEvent(),在ACTION_DOWN中调用requestDisallowInterceptTouchEvent(true),然后在ACTION_MOVE中根据条件去判断父容器是否需要拦截,若需要的话,则调用requestDisallowInterceptTouchEvent(false),同时父容器也要默认拦截除了ACTION_DOWN以外的事件。注:为什么父容器不能拦截ACTION_DOWN呢?因为一旦父容器拦截了,则所有的事件都无法传递到子元素去,因为后续的ACTION_MOVE、ACTION_UP会默认交给父容器。
6. LinearLayout与RelativeLayout比较
1)LinearLayout和RelativeLayout在draw跟layout过程中耗时几乎一致,而在measure过程中RelativeLayout比LinearLayout耗时长,原因是RelativeLayout会对子View进行两次measure,因为RelativeLayout中子View的排列方式是基于彼此之间的依赖关系的,所以需要在横向和纵向上分别排序测量一次;
2)而LinearLayout只需要判断线性规则,然后进行测量就可以了;
3)RelativeLayout的子View如果高度和RelativeLayout不同,则会引发效率问题,当子View很复杂时,这个问题会更加严重。如果可以,尽量使用padding代替margin;
4)若LinearLayout使用了weight属性,也会measure两次。
7. RecyclerView缓存机制
1)Scrap:离开屏幕的可以复用的数据
2)Cache:最近离开屏幕的2个
3)Extension:用户自定义
4)RecyclerViewPool:默认为5个,Cache装不下的会放到这里来
应用架构
1. MVC
2. MVP
3. MVVM
Android网络
1. 用过的网络框架,它们的源码和架构设计
2. 如何处理网络缓存,Okhttp怎么做的
3. 设计一个网络请求框架需要考虑什么
4. Socket和Websocket区别和使用
5. OkHttp源码相关(调度器、拦截器)
(1)OkHttp基本实现原理:OkHttp主要是通过5个拦截器、3个双端队列工作的,它的内部实现是通过责任链模式完成的,将网络请求的各个阶段封装在各个链条中,实现了各层的解耦。
(2)5个拦截器分别是:
- RetryAndFollowUpInterceptor: 重试和重定向拦截器,从头部的Location中获取一个新的url,重新发起一次请求。
- BridgeInterceptor: 应用层和网络层的桥接拦截器,主要工作是为请求添加cookie、以及一些通用的请求头,如Host、Content-Length、Content-Type、User-Agent等。
- CacheInterceptor: 缓存拦截器,读取本地缓存的数据,如果有缓存,则要先判断是否有缓存策略,然后判断是否过期,如果没有过期则可以直接从缓存中读取。若服务器返回304,则代表资源未修改,客户端可以直接从本地缓存中拿数据。
- ConnectInterceptor: 连接拦截器,内部会维护一个连接池,负责复用连接、创建连接、释放连接以及创建连接上的socket流。
- CallServerInterceptor: 请求拦截器,发起网络请求,给服务器写数据和读取数据。
(3)3个双端队列分别是:异步任务队列、同步任务队列、异步任务等待队列
(4)如何实现网络缓存:目前OkHttp只支持get请求的缓存
6. Retrofit原理
- 使用了动态代理,把传进去的ApiService.class返回一个动态代理对象,然后通过调用这个对象里的方法,返回一个Call对象,然后交给OkHttp去请求。
- 使用了Java的反射,Retrofit会用反射去获取到要执行的方法的注解信息,然后创建一个ServiceMethod对象,ServiceMethod就像一个中央处理器,传入Retrofit对象和Method对象,通过调用各个接口和解析器,最终生成一个Request,最后返回一个Call对象。
Android图片加载
1. 大图如何防止OOM
- 采样压缩
1)使用inSampleSize进行按比例压缩,此值只能设置为2的幂次方,注意:要使用inSampleSize,需要先将inJustDecodeBounds的值设为true;
2)当加载长图时进行局部解码,使用BitmapRegionDecoder类,可以解码图像中的某一块矩形区域。
- 质量压缩
1)通过改变quality值(0-100),如果图片格式是PNG的话,则改变此值不会改变图片文件大小,因为PNG格式的图片是无损的;
2)使用google推出的webp格式的图片,能够节省25%-34%的空间。
2. 设计一个图片加载框架需要考虑什么?
1)异步加载图片:线程池(至少两个)
2)切换线程:Handler
3)缓存问题:LruCache、DiskLruCache
4)防止OOM:软引用、LruCache设置大小、图片压缩、Bitmap存储位置
5)内存泄漏:ImageView的正确引用、生命周期的管理(如:Activity销毁后应该取消图片加载任务)
6)列表滑动加载的问题:加载错乱、任务队列过多问题
3. 用的比较多的图片加载库:如Glide,它对以上需要考虑的东西是怎么处理的?
1)线程池:Glide中有3个线程池,分别是用于加载网络图片的、加载内存及硬盘缓存图片的、加载动画的,由于网络会阻塞,所以网络缓存跟内存硬盘缓存要使用不同的线程池;
2)切换线程:当图片异步加载成功后,需要通过Handler切换到主线程去更新ui;
3)缓存问题:Glide中有三级缓存,分别是内存缓存、硬盘缓存、网络缓存,而内存缓存是通过LruCache实现的,硬盘缓存是通过DiskLruCache实现的,它们都是通过LinkedHashMap实现的,LinkedHashMap继承自HashMap,同样是采用数组+链表的混合结构实现的,但这是一个双向链表结构,可以把正在最新使用的数据添加到链表头,并且会在put数据时会先判断当前的内存是否超过限定的内存大小,若超过,则移除最老的数据;
4)防止OOM:设置LruCache的缓存大小、使用软引用修饰Bitmap、通过onLowMemory()方法清除缓存、使用RGB_565来创建Bitmap,减少内存开销、把Bitmap像素数据存放在native堆;
5)内存泄漏:通过弱引用去修饰ImageView(解决得并不完美)、Glide通过监听生命周期的回调,在Activity/fragment 销毁的时候,取消图片加载任务;
6)列表加载问题:由于RecyclerView的复用机制,ImageView可能会由于复用而造成数据错乱的问题,解决方法是给ImageView设置Tag,tag一般是图片地址,更新ImageView之前判断tag是否跟url一致。
4. Glide、ImageLoader、Fresco、Picasso的差异和优缺点
- Glide优点
1)多种图片格式的缓存,如Gif、WebP、缩略图等
2)集成了生命周期,可以根据Activity或者Fragment的生命周期管理图片加载请求
3)高效处理Bitmap
4)高效的缓存策略,可以缓存多种规格(Picasso只能缓存原始尺寸)
5)加载速度快且内存开销小(内存开销是Picasso的一半,默认色彩通道是RGB565)
- ImageLoader缺点
1)不支持gif,使用较繁琐
- Fresco优缺点
1)优点:在5.0以下图片存储在Android系统的匿名共享内存(Ashmem区),而不是虚拟机的堆内存,所以不会因为加载图片而导致OOM;
2)缺点:框架体积较大,影响apk大小,并且使用起来较繁琐。
- Picasso缺点:
1)默认使用ARGB_8888格式,生成的图片占用内存较大;
2)缓存的图片格式是原始尺寸;
5. Glide是怎么与context绑定生命周期的?
1)创建一个无UI的fragment,也就是SupportRequestManagerFragment,并绑定到当前Activity,便于感知当前的Activity的生命周期变化;
2)在创建SupportRequestManagerFragment时,会初始化ActivityFragmentLifecycle、LifecycleListener,并在生命周期的onStart()、onStop()、onDestory()中调用相关方法;(其中ActivityFragmentLifecycle的作用是:保存fragment跟RequestManager的映射关系,并管理LifecycleListener)
3)把步骤2中的lifecycle传入RequestManager中,并且RequestManager实现了LifecycleListener接口;
4)最后当生命周期变化时,就可以通过LifecycleListener接口回调去通知RequestManager去处理相关操作了。
6. LruCache的实现原理
Android动画
1. 属性动画、补间动画、帧动画的区别以及使用场景
2. Lottie
3. Android 3.0以前对于属性动画的兼容
4. 插值器(使用了策略模式)
- 线性
- 加速
- 减速
- 先加速后减速
Android数据存储
1. 文件存储
2. SharedPrefrence以及它的性能问题
- 通过getXXX()方法获取数据,可能会导致主线程阻塞。因为getXXX()方法都是同步的,而在主线程调用get方法,必须等待sp加载完成,而sp加载过程是异步的,如果需要加载一个比较大的数据,则会造成主线程阻塞。
- sp不能保证类型安全。使用相同的key进行操作的时候,会把原来的数据覆盖掉,从而在读取数据的时候会出现ClassCastException异常。
- sp加载的数据会一直留在内存中。sp的数据是通过静态的ArrayMap进行存储的。
- apply()方法是异步的,可能会造成ANR。当生命周期处于handleStopService()、handlePauseActivity()、handleStopActivity()时,会一直等待apply()方法将数据保存成功,否则会一直等待,从而阻塞主线程造成ANR。
- apply()方法无法获取返回结果。
3. SQLite:版本升级,读写优化,数据迁移
4. 用过的数据库框架以及区别
热修复
1. AndFix
2. Robust
3. Tinker
网络
TCP、UDP
1. TCP/IP协议栈
1)应用层:HTTP、FTP、SMTP
2)传输层:TCP、UDP
3)网络层:IP协议、路由
4)网络接口层:负责接收ip数据包并通过网络发送(帧、网络接口协议)
2. OSI协议栈
1)应用层
2)表示层
3)会话层
4)传输层
5)网络层
6)数据链路层:数字信号和光/电信号互转、
7)物理层:光纤、双绞线
3. TCP的三次握手以及四次挥手
4. TCP与UDP的区别及使用场景
HTTP
1. HTTP报文结构及状态码
2. HTTP 1.0 1.1 2.0的区别
- 版本1.1与版本1.0的区别:http1.0的每一个请求都必须要重新连接,不仅耗时还耗资源,而http1.1默认开启长连接,一个连接可以发送多个请求,但是这会造成队头阻塞问题,也就是说当一个队头的请求不能收到响应的资源时,它将会阻塞后面的请求,而减轻这个问题的方法是添加多个TCP连接,但又会造成资源开销问题。
- 版本2.0与版本1.1的区别:
(1)针对http1.1的多个TCP连接问题,http2使用了多路复用,http2在两端建立一个单独的连接,该连接包含多个数据流。 每个流包含多个请求/响应格式的消息,最终,每个消息被划分为更小的帧单元。
(2)头部数据压缩:在HTTP1.1中,HTTP请求和响应都是由状态行、请求/响应头部、消息主体三部分组成。一般而言,消息主体都会经过gzip压缩,或者本身传输的就是压缩过后的二进制文件,但状态行和头部却没有经过任何压缩,直接以纯文本传输,HTTP2.0使用HPACK算法对header的数据进行压缩,这样数据体积小了,在网络上传输就会更快。
3. HTTP与HTTPS区别,HTTPS如何保证安全
- Http:使用明文传输数据,很容易被监听者监听并篡改数据;
- Https是如何保证安全的:
1)通过对称加密与非对称加密结合,浏览器会随机生成一个密钥,然后用网站提供的公钥对密钥进行加密,把密钥传输到网站后,网站通过自己的私钥去解密;
2)第一步传输密钥的时候是使用非对称加密实现的,当密钥传输过去后,以后的过程都是使用对称加密,所以不存在效率方面的影响;
3)由于公钥是公开的,所以存在会被人恶意修改密钥的情况,在此基础上通过CA证书来解决这个问题,CA机构在制作证书时,除了网站的公钥外,还要包含其他数据,用来辅助校验,比如说网站域名。
Jetpack
LifeCycle
- 概念:LifeCycle可以帮助开发者创建可感知生命周期的组件,然后由组件在其内部管理自己的生命周期,从而降低模块间的耦合度,并降低内存泄漏的风险。
- 原理:Jetpack为我们提供的两个类,分别是:LifeCycleOwner和LifeCycleObserver,也就是通过观察者模式,实现对页面生命周期的监听。
- 使用:
1)对于Activity/Fragment:由于在SupportActivity以及Fragment中已经实现了LifeCycleOwner接口,所以可以直接创建一个类实现LifecycleObserver接口,然后去使用@OnLifecycleEvent标签进行标识,当生命周期发生变化时,
会自动调用被标识过的方法,最后通过getLifecycle.addObserver()将观察者与被观察者绑定。
2)对于Service:在LifecycleService里实现了LifecycleOwner接口,所以当创建一个Service的时候,需要extends LifecycleService,然后创建一个类实现LifecycleObserver接口,其他的步骤同上。
3)对于Application:LifeCycle提供了一个名为ProcessLifecycleOwner类,用于监听Application的生命周期,常用于监听应用程序进入前台或者退出到后台的情况,使用方法同上。注:Lifecycle.Event.ON_DESTORY永远不会调用,因为系统不会分发这个事件。
DataStore
- SharePreferences的坑?
1)通过getXXX()方法获取数据,可能会导致主线程阻塞。因为getXXX()方法都是同步的,而在主线程调用get方法,必须等待sp加载完成,而sp加载过程是异步的,如果需要加载一个比较大的数据,则会造成主线程阻塞。
2)sp不能保证类型安全。使用相同的key进行操作的时候,会把原来的数据覆盖掉,从而在读取数据的时候会出现ClassCastException异常。
3)sp加载的数据会一直留在内存中。sp的数据是通过静态的ArrayMap进行存储的。
4)apply()方法是异步的,可能会造成ANR。当生命周期处于handleStopService()、handlePauseActivity()、handleStopActivity()时,会一直等待apply()方法将数据保存成功,否则会一直等待,从而阻塞主线程造成ANR。
5)apply()方法无法获取返回结果。
- DataStrore解决的问题?
1)
2)
3)
4)
- 如何从SharePreferences迁移到DataStrore?
Navigation
- 作用:方便我们在一个Activity中对多个Fragment进行管理。
- 主要元素:
1)Navigation Graph:是一种新型的XML文件,其中包含应用程序所有的页面,以及页面间的关系。
2)NavHostFragment:是其他Fragment的容器,用于展示Fragment。
3)NavController:用于完成Navgation Graph中具体的页面切换工作。
- 使用:
1)首先在res下创建navigation包,然后创建nav_graph.xml文件,然后通过在Design页面点击Create new destination来创建fragment;
2)当创建多个fragment之后,可以在Design面板上通过箭头标注页面的切换方向,也可以添加切换动画,这样会在nav_graph.xml中创建了action以及animation节点,通过在代码中调用action的id去进行切换页面;
3)可以通过传统的Bundle方式传递数据,然后把bundle对象传给NavController,也可以通过引入safe args插件去进行传递数据,使用safe args插件的好处在于插件为我们生成的代码文件里包含了参数对应的Getter跟Setter方法,保证了参数的类型安全。
- NavigationUI:方便App Bar中的按钮与菜单能够与导航图中的页面关联起来。
- 深层链接DeepLink有两种应用场景:
1)PendingIntent:当应用程序接收到某个通知时,需要直接跳转到展示该通知的页面,就可以通过PendingIntent来实现;
2)URL:当用户通过手机浏览器浏览某个页面时,可以通过“在应用内打开”来打开我们的应用程序,如果没有安装的话就引导用户安装应用程序。此方式需要在导航图上添加<deepLink/>,然后在清单文件中为该Activity设置<nav-graph/>标签。
ViewModel
- 概念:ViewModel是介于View跟Model之间的桥梁,使视图跟数据既能够分离开,也能够保持通信。
- 当旋转屏幕导致Activity重建时,不会影响ViewModel的生命周期。
- 当使用ViewModel时,不能将任意类型的Context对象传入ViewModel,否则会造成内存泄漏,若非要在ViewModel中使用Context,则可以使用AndroidViewModel类,它继承自ViewModel,并且接收Application作为Context。
- ViewModel跟onSaveInstanceState()方法的区别:onSavaInstanceState()只能保存少量的、能支持序列化的数据,而ViewModel没有这个限制;但是ViewModel不支持数据持久化,当页面彻底销毁后,ViewModel及其持有的数据就不存在了,而onSaveInstanceState()可以持久化页面的数据。