八股文面筋

231 阅读21分钟

0. 数据结构

1. HashMap

hash 冲突时的链表尾插;

  • 在 JVM 1.8 之后有所优化:

当链表长度超过8的时候,会根据当前的hash值,将链表结构传化为红黑树的结构。

n个元素,查找的时间复杂度为 lg n

红黑树是一种含有红黑结点并能自平衡的二叉查找树。它必须满足下面性质:

  1. 每个节点要么是黑色,要么是红色。
  2. 根节点是黑色。
  3. 每个叶子节点(NIL)是黑色。
  4. 每个红色结点的两个子结点一定都是黑色。
  5. 任意一结点到每个叶子结点的路径都包含数量相同的黑结点。

image

2. 线程安全的 HashTable 和 ConcurrentHashMap

Hashtable之所以效率低下主要是因为其实现使用了synchronized关键字对put等操作进行加锁,而synchronized关键字加锁是对整个对象进行加锁,也就是说在进行put等修改Hash表的操作时,锁住了整个Hash表,从而使得其表现的效率低下;因此,在JDK1.5~1.7版本,Java使用了分段锁机制实现ConcurrentHashMap.

简而言之,ConcurrentHashMap在对象中保存了一个Segment数组,即将整个Hash表划分为多个分段;而每个Segment元素,即每个分段则类似于一个Hashtable;这样,在执行put操作时首先根据hash算法定位到元素属于哪个Segment,然后对该Segment加锁即可。因此,ConcurrentHashMap在多线程并发编程中可是实现多线程put操作。

ConcurrentHashMap 的数据结构:

image.png

ConcurrentHashMap类结构如上图所示。由图可知,在ConcurrentHashMap中,定义了一个Segment<K, V>[]数组来将Hash表实现分段存储,从而实现分段加锁;而么一个Segment元素则与HashMap结构类似,其包含了一个HashEntry数组,用来存储Key/Value对。Segment继承了ReetrantLock,表示Segment是一个可重入锁,因此ConcurrentHashMap通过可重入锁对每个分段进行加锁。

SparseArray

Key 必须为 int 类型,内部实现为两个数组,Key[] 和 Value[],Key数组按Key值大小从小到大进行排序,查找的时候使用二分查找;对应的Key找到index,则 Value 也在 Value 数组的对应顺位;在每次删除后都会预备GC,总是尝试保持数据分布在数组的前面。

SparseArray有两个优点:

  1. 避免了自动装箱(auto-boxing)
  2. 数据结构不会依赖于外部对象映射。

WeakHashMap

键值为弱引用。若外部不再引用的时候,在GC的时候被回收,Map 内对应的键值对也就自动删除。

用法举例:WeakHashMap<View,String> ,View 因为绑定了 activity 的上下文对象,若存放在生命周期较长的Map内,容易出现内存泄漏。

实现:大体原理跟HashMap 一致。不同的是他的内部 Entry 为 WeakReference 子类,将 Key 作为弱引用维持对象,并且指定了 ReferenceQueue,在 弱引用被回收的时候,就会进入引用队列。在发生 resize,等操作的时候,遍历 ReferenceQueue,将回收的Key 对应的 Entry 从数组中删去。

HashSet

元素不能重复的一个集合类数据模型,内部元素排列为无序。

实现:内部使用一个 HashMap 来存储数据,将存入的元素作为 HashMap 的 key,以达到存入元素无重复的目的;

算法

快速排序

Java 基础

多线程

ThreadLocal实现原理

ThreadLocal 是通过 ThreadLocalMap 来完成存储,ThreadLocalMap 的 key 是 ThreadLocal,值是该 ThreadLocal 指向 T 对象。每个线程内都持有了自己的 ThreadLocalMap。在 ThreadLocal 进行存取的时候,会获取当前线程对象,通过当前线程对象拿到它持有的 ThreadLocalMap 对象,最终通过 ThreadLocalMap 来完成存取

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

synchronized

把代码块声明为 synchronized,有两个重要后果,通常是指该代码具有 原子性(atomicity)和 可见性(visibility)。

  • 原子性 原子性意味着个时刻,只有一个线程能够执行一段代码,这段代码通过一个monitor object保护。从而防止多个线程在更新共享状态时相互冲突。

  • 可见性 可见性则更为微妙,它要对付内存缓存和编译器优化的各种反常行为。它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 。

    作用:如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题

    原理:当对象获取锁时,它首先使自己的高速缓存无效,这样就可以保证直接从主内存中装入变量。 同样,在对象释放锁之前,它会刷新其高速缓存,强制使已做的任何更改都出现在主内存中。 这样,会保证在同一个锁上同步的两个线程看到在 synchronized 块内修改的变量的相同值。

可重入锁 和 自旋锁

  • 可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。 在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁。

    可重入锁最大的作用是避免死锁。

  • 自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

    通过 CAS算法 即compare and swap(比较与交换),可以很快的实现一个自旋锁;

        public class SpinLock {
            private AtomicReference<Thread> cas = new AtomicReference<Thread>();
            public void lock() {
                Thread current = Thread.currentThread();
                // 利用CAS
                while (ture) {
                    if(cas.compareAndSet(null, current)){
                        return;
                    }
                    // DO nothing, just wait
                }
            }
            public void unlock() {
                Thread current = Thread.currentThread();
                cas.compareAndSet(current, null);
            }
        }
    

线程池

任务被提交到线程池,会先判断当前线程数量是否小于corePoolSize,如果小于则创建线程来执行提交的任务,否则将任务放入workQueue队列,如果workQueue满了,则判断当前线程数量是否小于maximumPoolSize,如果小于则创建线程执行任务,否则就会调用handler,以表示线程池拒绝接收任务。

  • orePoolSize:线程池的核心线程数,说白了就是,即便是线程池里没有任何任务,也会有corePoolSize个线程在候着等任务。
  • maximumPoolSize:最大线程数,不管你提交多少任务,线程池里最多工作线程数就是maximumPoolSize。
  • keepAliveTime:线程的存活时间。当线程池里的线程数大于corePoolSize时,如果等了keepAliveTime时长还没有任务可执行,则线程退出。
  • unit:这个用来指定keepAliveTime的单位,比如秒:TimeUnit.SECONDS。
  • workQueue:一个阻塞队列,提交的任务将会被放到这个队列里。
  • threadFactory:线程工厂,用来创建线程,主要是为了给线程起名字,默认工厂的线程名字:pool-1-thread-3。
  • handler:拒绝策略,当线程池里线程被耗尽,且队列也满了的时候会调用。 handler:表示当拒绝处理任务时的策略,也是可以自定义的,默认是我们前面的4种取值:
    • ThreadPoolExecutor.AbortPolicy(默认的,一言不合即抛异常的)
    • ThreadPoolExecutor.DiscardPolicy(一言不合就丢弃任务)
    • ThreadPoolExecutor.DiscardOldestPolicy(一言不合就把最近的任务给抛弃,然后执行当前任务)
    • ThreadPoolExecutor.CallerRunsPolicy(由调用者所在线程来执行任务)

image.png

  • 线程池的三种队列区别:SynchronousQueue、LinkedBlockingQueue 和ArrayBlockingQueue

    1. SynchronousQueue(CachedThreadPool) 类似交警只是指挥车辆,并不管理车辆 SynchronousQueue没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素。 超出直接corePoolSize个任务,直接创建新的线程来执行任务,直到(corePoolSize+新建线程)> maximumPoolSize。不是核心线程就是新建线程。

    2. LinkedBlockingQueue(single,fixed)类似小仓库,暂时存储任务,待系统有空的时候再取出执行 BlockingQueue是双缓冲队列。BlockingQueue内部使用两条队列,允许两个线程同时向队列一个存储,一个取出操作。在保证并发安全的同时,提高了队列的存取效率。 LinkedBlockingQueue是一个无界缓存等待队列。当前执行的线程数量达到corePoolSize的数量时,剩余的元素会在阻塞队列里等待。(所以在使用此阻塞队列时maximumPoolSizes就相当于无效了),每个线程完全独立于其他线程。生产者和消费者使用独立的锁来控制数据的同步,即在高并发的情况下可以并行操作队列中的数据。

    3. ArrayBlockingQueue ArrayBlockingQueue是一个有界缓存等待队列,可以指定缓存队列的大小,当正在执行的线程数等于corePoolSize时,多余的元素缓存在ArrayBlockingQueue队列中等待有空闲的线程时继续执行,当ArrayBlockingQueue已满时,加入ArrayBlockingQueue失败,会开启新的线程去执行,当线程数已经达到最大的maximumPoolSizes时,再有新的元素尝试加入ArrayBlockingQueue时会报错

JVM

image.png

Java代码会有源代码(java)经过编译器编译成class文件(二进制字节码),然后由JVM虚拟机去解释执行,执行前会对 class 文件进行校验,通过后再由类加载器加载,再用JIT编译器去解析成平台机器码去执行。

类加载

  • 加载:它是 Java 将字节码数据从不同的数据源读取到 JVM 中,并映射为 JVM 认可的数据结构(Class 对象)。如果输入数据不是 ClassFile 的结构,则会抛出 ClassFormatError。加载阶段是用户参与的阶段,我们可以自定义类加载器,去实现自己的类加载过程。
  • 链接:这是核心的步骤,简单说是把原始的类定义信息平滑地转入 JVM 运行的过程中。这里可进一步细分成三个步骤:
    • 验证:这是虚拟机安全的重要保障,JVM 需要核验字节信息是符合 Java 虚拟机规范的,否则就被认为是 VerifyError,这样就防止了恶意信息或者不合规信息危害 JVM 的运行,验证阶段有可能触发更多 class 的加载。
    • 准备(Pereparation),创建类或者接口中的静态变量,并初始化静态变量的初始值。但这里的“初始化”和下面的显示初始化阶段是有区别的,侧重点在于分配所需要的内存空间,不会去执行更进一步的 JVM 指令。
    • 解析(Resolution),在这一步会将常量池中的符号引用(symbolic reference)替换为直接引用。在 Java 虚拟机规范中,详细介绍了类,接口,方法和字段等各方面的解析。
  • 初始化:这一步真正去执行类初始化的代码逻辑,包括静态字段赋值的动作,以及执行类定义中的静态初始化块内的逻辑,编译器在编译阶段就会把这部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。
  • (使用)(卸载)

再来谈谈双亲委派模型,简单说就是当加载器(Class-Loader)试图加载某个类型的时候,除非父类加载器找不到相应类型,否则尽量将这个任务代理给当前加载器的父加载器去做。使用委派模型的目的是避免重复加载 Java 类型。

内存

JVM内存大致可分为:方法区、虚拟机栈、本地方法栈、堆、程序计数器;

image.png

  • 方法区:方法区是被所有线程共享的区间,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码
  • JVM 虚拟机栈:Java 虚拟机栈为 JVM 执行 Java 方法服务。栈中存储的栈帧(先进后出),每个方法在执行时都会创建一个栈帧。栈帧中存储了局部变量表、操作数栈、动态连接和方法出口等信息。
  • 本地方法栈:本地方法栈则为 JVM 使用到的 Native 方法服务,Native 方法不是以 Java 语言实现的,而是以本地语言实现的(比如 C 或 C++)
  • 堆:Java虚拟机所管理的内存中最大的一块存储区域。堆内存被所有线程共享。主要存放使用new关键字创建的对象。所有对象实例以及数组都要在堆上分配。
  • 程序计数器:是一块较小的内存空间,可以看作是当前线程所执行字节码的行号指示器,指向下一个将要执行的指令代码,由执行引擎来读取下一条指令。更确切的说,一个线程的执行,是通过字节码解释器改变当前线程的计数器的值,来获取下一条需要执行的字节码指令,从而确保线程的正确执行。

垃圾回收

垃圾判断算法

  • 引用计数法:给每个对象添加一个计数器,当有地方引用该对象时计数器加1,当引用失效时计数器减1。用对象计数器是否为0来判断对象是否可被回收。缺点:无法解决循环引用的问题

  • 可达性分析算法:通过GC ROOT的对象作为搜索起始点,通过引用向下搜索,所走过的路径称为引用链。通过对象是否有到达引用链的路径来判断对象是否可被回收(可作为GC ROOT的对象:虚拟机栈中引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中JNI引用的对象)

垃圾回收算法

  • 标记-清除算法
  • 复制算法
  • 标记-整理算法:标记整理算法解决了“标记-清除”内存碎片的问题
  • 分代收集算法:新生代-复制;老年代-标记-清除or整理

引用

  • 强引用(StrongReference):存在强引用的对应,不会被JVM回收
  • 弱引用(WeakReference):存在弱引用的对象,每次JVM进行垃圾回收时,该对象都会被回收。 应用:短时间缓存某些次要数据。
  • 软引用(SoftReference):存在软引用的对象,在内存不足时,才会被JVM回收。 应用:缓存数据,提高数据的获取速度。(图片缓存)
  • 虚引用(PhantomReference):相当于无引用,使对象无法被使用,必须与引用队列配合使用。 应用:使对象进入不可用状态,等待下次JVM垃圾回收,从而使对象进入引用列队中。

引用队列(ReferenceQueue)

效果:引用队列可以配合软引用、弱引用及幽灵引用使用,当引用的对象将要被JVM回收时,会将其加入到引用队列中。 应用:通过引用队列可以了解JVM垃圾回收情况。

// 引用队列
ReferenceQueue<String> rq = new ReferenceQueue<String>();
 
// 软引用
SoftReference<String> sr = new SoftReference<String>(new String("Soft"), rq);
// 弱引用
WeakReference<String> wr = new WeakReference<String>(new String("Weak"), rq);
// 幽灵引用
PhantomReference<String> pr = new PhantomReference<String>(new String("Phantom"), rq);
 
// 从引用队列中弹出一个对象引用
Reference<? extends String> ref = rq.poll();

网络

Http协议是超文本传输协议,请求分成三个部分,分别是请求行,消息报头,请求正文

HTTP1.0与2.0的区别

HTTP1.0:浏览器每次请求都需要与服务器建立一个TCP连接,服务器处理完成后立即断开TCP连接(无连接),服务器不跟踪每个客户端也不记录过去的请求(无状态)

HTTP2.0:HTTP2.0引入了二进制数据帧和流的概念,其中帧对数据进行顺序标识,这样浏览器在收到数据之后,就可以按照序列对数据进行合并,而不会出现合并后数据错乱的情况。同样因为有了序列,服务器就可以并行的传输数据,这就是流所做的事情。

https

https link

image.png

https 随机秘钥(session key)的产生过程:

第一步握手中,客户端发送 client_random 给服务端, 第二步握手中,服务端返回 证书公钥 和 server_random 给客户端。 他们交换了各自产生的随机数。

第三步,客户端还会生成一个 pre-master-key 的随机数,这是出现的第三个随机数,该随机数会用服务端的公钥加密,然后传给服务端。

然后client和server会使用一个PRF(Pseudo-Random Function)方法来产生 master-secret。 在产生的过程中,上述三个随机数,都会被用到。 master_secret = PRF(pre_master_secret, "master secret", ClientHello.random + ServerHello.random)

随后,客户端和服务端都是通过该 master-secret 来生成对称加密秘钥(session key)。

证书链如何校验?

当前证书中的证书基本信息,做hash值计算,并使用上级证书机构的私钥签名。在做证书校验的时候,用上级证书中的公钥可以验证上级证书颁发机构的合法性。每个证书合法性由其签发的上级证书做担保。

架构和设计模式

代理模式

静态代理

动态代理

跨平台方案

Flutter

通道 Channel

Flutter与Native的通信是通过Channel实现的。实际上这个 Channel 本质是在C++层维护的一个map数据结构,由key-value的形式对channel name和handler进行映射。Dart VM通过C接口与C++对象进行通信,iOS原生对C++混编有良好的支持,而Android则是通过JNI的形式与C++进行通信。通过这种形式,Flutter能够与Native基于Channel的这种抽象进行无缝的通信。

官方实现的Channel会在通信过程当中对Dart的类型与Objective-C,Java类型进行转换。所以使用起来还是挺方便,但是对于一些自定义的类型,在转换过程当中有可能会出现一些不可预期的问题。

性能优化篇

安卓基础

基础

Activity 启动模式

  • singleTop,栈顶不是该类型的Activity,创建一个新的Activity。否则,onNewIntent。

  • singleTask,回退栈中没有该类型的Activity,创建Activity,否则,onNewIntent+ClearTop。

  • singleInstance,回退栈中,只有这一个Activity,没有其他Activity。

    注意:

    1. 设置了"singleTask"启动模式的Activity,它在启动的时候,会先在系统中查找属性值affinity等于它的属性值taskAffinity的Task存在; 如果存在这样的Task,它就会在这个Task中启动,否则就会在新的任务栈中启动。因此, 如果我们想要设置了"singleTask"启动模式的Activity在新的任务中启动,就要为它设置一个独立的taskAffinity属性值。
    2. 如果设置了"singleTask"启动模式的Activity不是在新的任务中启动时,它会在已有的任务中查看是否已经存在相应的Activity实例, 如果存在,就会把位于这个Activity实例上面的Activity全部结束掉,即最终这个Activity 实例会位于任务的Stack顶端中。
    3. 在一个任务栈中只有一个”singleTask”启动模式的Activity存在。他的上面可以有其他的Activity。这点与singleInstance是有区别的。

应用场景:

  • singleTop 适合接收通知启动的内容显示页面。例如,某个新闻客户端的新闻内容页面,如果收到10个新闻推送,每次都打开一个新闻内容页面是很烦人的。

  • singleTask 适合作为程序入口点。例如浏览器的主界面。不管从多少个应用启动浏览器,只会启动主界面一次,其余情况都会走onNewIntent,并且会清空主界面上面的其他页面。

  • singleInstance 应用场景: 闹铃的响铃界面。 你以前设置了一个闹铃:上午6点。在上午5点58分,你启动了闹铃设置界面,并按 Home 键回桌面;在上午5点59分时,你在微信和朋友聊天;在6点时,闹铃响了,并且弹出了一个对话框形式的 Activity(名为 AlarmAlertActivity) 提示你到6点了(这个 Activity 就是以 SingleInstance 加载模式打开的),你按返回键,回到的是微信的聊天界面,这是因为 AlarmAlertActivity 所在的 Task 的栈只有他一个元素, 因此退出之后这个 Task 的栈空了。如果是以 SingleTask 打开 AlarmAlertActivity,那么当闹铃响了的时候,按返回键应该进入闹铃设置界面。

View 触摸事件分发

note.youdao.com/noteshare?i…

安卓 Resource 资源如何寻址

Android资源管理框架实际就是由AssetManager和Resources两个类来实现的。其中,Resources类可以根据ID来查找资源,而AssetManager类根据文件名来查找资源。事实上,如果一个资源ID对应的是一个文件,那么Resources类是先根据ID来找到资源文件名称,资源ID与资源名称的对应关系是由打包在APK里面的resources.arsc文件中,然后再将该文件名称交给AssetManager类来打开对应的文件的

IBinder 机制

blog.csdn.net/carson_ho/a…

白话:安卓的Binder机制用于实现跨进程通信(IPC);出于安全性&稳定性目的,Android的进程是相互独立、隔离的。一个进程空间分为 用户空间 & 内核空间,用户空间的内存不可共享,而内科空间是可共享空间。进程内,用户空间和内核空间的数据交互需要通过系统调用 copy_from_user()copy_to_user()

回到 Binder,Binder 跨进程通信机制模型 基于 Client - Server, 在进程通信中衔接了 Client 进程、Server 进程、以及 ServiceManager 进程。它通过内存映射传递进程间的数据。

  1. Binder驱动在内核区创建一个 接收缓存区;
  2. 实现地址映射关系,即 实现 内核缓存区和接收进程用户空间地址 同步映射到 同一个共享接收缓存区。
  3. 发生进程通过 copy_from_user 发生数据到内核缓存区,发生1次数据拷贝
  4. 由于内核缓存区 和 接收进程的用户空间地址 存在内存映射关系,故相当于也发生到了接收进程的用户空间地址,实现了跨进程通信。

image

app 启动流程

[Activity 启动流程(blog.csdn.net/qijinglai/a…) 看这篇

调优

第三方框架&技术栈

butterknife & 注解 APT

涉及到3个核心技术

  • 编译期注解 (注解生命周期 Retention:SOURCE/CLASS/RUNTIME)
  • APT(注解处理器)
  • javaPoet(自动生成代码)

LinkCanary

在 Application 内初始化,会增加一个 ActivityLifeCycleCallBack,每启动一个 Activity ,会包装一个 KeyedWeakReference,并制定其 ReferenceQueue,用于观察回收情况。 在每个 Activity 被触发 onDestroy 的时候,对 ReferenceQueue 队列中的回收对象进行判断,若无销毁 Activity 的引用,则进行强行 GC,调用 Runtime.getRuntime().gc(); 完了,再观察引用回收队列,若还是没有发现 销毁 Activity 引用,则认为是 发生内存泄漏,调用 heapDumper.dumpHeap()生成.hprof文件,再用 HAHA 库分析 hprof 文件生成Snapshot对象(HAHA 是一个由 square 开源的 Android 堆分析库)。Snapshot用以查询对象的最短引用链。

BlockCanary

每个Android应用程序都有一个主线程ActivityThread,这个主线程会创建一个Looper(Looper.prepare),而Looper又会关联一个MessageQueue,主线程Looper会在应用的生命周期内不断轮询(Looper.loop),从MessageQueue取出Message 更新UI。

Looper 的 looper 函数内,其实是一个死循环,不停的从它的 MessageQueue 中取消息来交由对应的 Handler 处理。

并且在 dispatchMessage 的前后,分别有一个 Printer 对象,在进行日志打印,只需要自定义一个 Printer 代理对象,对事件处理前后的时间间隔进行监控,若过长,则说明主线程做的事情太多,出现卡顿。

ARouter

如何计算方法耗时

统计方法耗时是典型的面向切面编程(Aspect-Oriented Programming,AOP)的应用场景。实现AOP有一些成熟的技术方案:

  • 静态代理 和 运行期注解 + 动态代理
  • 编译时代码生成(APT),案例:ButterKnife,Dagger2,Room
  • 切面编程库(AspectJ),案例:Hugo
  • 字节码注入(ASM),案例:GrowingIO