其他面经汇总 (Android-深入)

解释一下环境变量是什么,作用是什么?

环境变量其实就是操作系统或者运行环境在启动时设置的一些变量,它们存储了一些配置或状态信息,用来告诉我们的应用程序在哪些条件下启动或运行。比如说,当我们启动一个应用时,程序可以通过环境变量获取一些外部配置信息,而不必在代码中硬编码这些参数,从而使得程序更加灵活和易于配置。

从Android开发的角度来看,虽然我们在应用中并不像服务器那样频繁使用环境变量,但了解它们仍然很重要。环境变量在一些应用场景下能帮助我们实现配置和安全分离,比如在开发和生产环境中,我们可能会设置不同的配置,比如不同的API地址、调试开关或者日志级别。通过环境变量,我们可以在部署之前调节这些外部配置,而不需要重新编译代码。

对我来说,环境变量的作用还在于解耦应用程序和它运行的环境。例如,一个Android应用在开发过程中可能会使用模拟数据或者测试环境,而在发布时切换到真实的数据和服务器,这个过程中配置管理就显得特别重要。环境变量可以作为一个外部参数让我们更方便地控制应用的行为,避免把不必要的敏感信息(如API密钥、数据库连接信息等)写在代码中,从而增强安全性和可维护性。同时,使用环境变量能使我们在多人协作或者持续集成中更容易保证不同的构建和运行配置的一致性。

总的来说,我认为环境变量在开发过程中扮演了一个“桥梁”的角色,它将程序与运行环境动态地连接起来,带来了灵活性和安全性,并且对于后期的维护、部署和测试都有显著帮助。通过环境变量,我们可以在不同的平台间轻松切换,并且在出现问题时快速定位错误所在的配置。

你是怎么去学习网络协议的,网络协议的定义是什么,说一下网络协议的三个要素?

通过计算机网络书籍学习

至于网络协议的定义,我认为,网络协议实质上是一组规则和约定,规定了两台或多台设备在网络上进行通信时如何格式化、传输和解释数据。这些规则确保了不同设备、不同系统之间能够正确地协同工作,实现数据交换与互操作。

具体到网络协议的三个核心要素,我的理解是:

  1. 语法(Syntax):也就是数据格式、结构和编码方式。它规定了通信双方传输的数据包的格式,比如头部、负载和尾部的构成,以及各字段如何排列和编码。这一点非常关键,因为一旦数据格式不统一,就无法进行正确解析。

  2. 语义(Semantics):这部分关注的是数据传输中的含义,比如每个字段或数据段所代表的具体意义。它规定了当一方发送某个消息时,对方应如何解释和响应,比如在TCP协议中,SYN表示建立连接的请求,ACK表示确认连接;在HTTP中,状态码200代表成功、404代表资源未找到等,都是语义传达的重要体现。

  3. 同步(Timing):也有时候称为顺序或者时序控制,主要是指在数据交换过程中,双方如何协调和管理通信的时序问题。它包括消息的发送、确认、重传、超时以及顺序控制等机制。没有良好的时序约定,可能会导致数据丢失、乱序或重复接收,从而影响通信的可靠性。

在实际项目中,我不仅会依靠这些理论知识,还在开发过程中尝试通过Java或者Kotlin进行网络编程,对这些协议进行封装与应用。比如在做HTTP请求时,我会关注响应头、状态码还有数据体的格式,并结合开源库一块深入研究它们是如何实现异步通信、重连策略以及错误处理的,这样的实践经验对我理解网络协议非常有帮助。

多线程如何并发执行的

首先,多线程的并发执行依赖于操作系统和JVM对线程调度的支持。每个线程都是一个独立的执行单元,操作系统会将它们分配给不同的CPU核心运行或者在单个核心上通过时间片切换实现并发。也就是说,真正的并行在多核环境下是可以实现的,但在单核环境中,多线程通过快速切换实现表面上的并发,使用户感觉到多个任务同时进行。

在Java和Kotlin中,我们可以通过多种方式来实现多线程,比如直接使用Thread类、Runnable接口,或者更常用的是通过Executor框架来管理线程池。使用线程池比较关键,它能有效减少线程的创建和销毁开销,避免频繁的资源申请,从而提高系统性能。例如,在Android开发中,很多异步操作、网络请求以及图片加载等场景,都会通过线程池来调度后台任务,确保主线程不被阻塞。

另一个需要注意的是,虽然线程并发带来了性能提升,但也引入了同步与数据一致性的问题。在并发执行时,多个线程可能会同时对共享数据进行访问或修改,这时就需要借助同步机制(如synchronized关键字或者Lock接口)来保证线程安全。当然,这会引入额外的性能损耗,所以在设计多线程架构时,要权衡好并发性能和数据一致性,比如通过减少锁的粒度或使用无锁的数据结构来优化性能。

从实际的开发角度看,多线程并发执行还涉及到任务的划分和协调。例如,我在开发中会根据任务的互相独立性来划分执行任务,对于彼此不依赖的任务,可以并发执行;而对于有先后顺序依赖的任务,则需要适当设置任务间的同步机制或者依赖关系。此外,线程间通信也是非常重要的一环,常见方式有消息传递(如Handler机制)、future/promise模式、

回调等方法,确保各个线程能在适当的时机交换信息或者触发后续操作。

最后,我认为在实际工作中,多线程并发不仅仅是技术的实现,更需要考虑整体的架构设计和资源管理。比如,在Android中,由于应用的生命周期和UI线程的限制,我们往往会将耗时操作放到后台线程,并通过异步回调或者观察者模式将结果传递到主线程更新UI。这样既能保证应用的响应速度,又能充分利用多线程带来的性能优势。

Runnable实现多线程

// 定义一个类,实现 Runnable 接口
public class MyRunnableTask implements Runnable {

    private String taskName;

    // 构造函数传入任务名称
    public MyRunnableTask(String taskName) {
        this.taskName = taskName;
    }

    // 重写 run() 方法,定义线程要执行的任务逻辑
    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println(taskName + " is running: Step " + i);
            try {
                // 模拟任务执行的耗时操作
                Thread.sleep(500); // 线程休眠 500 毫秒
            } catch (InterruptedException e) {
                System.out.println(taskName + " was interrupted.");
            }
        }
        System.out.println(taskName + " has completed.");
    }

    public static void main(String[] args) {
        // 创建多个 Runnable 对象
        MyRunnableTask task1 = new MyRunnableTask("Task 1");
        MyRunnableTask task2 = new MyRunnableTask("Task 2");

        // 创建线程,将 Runnable 对象传入 Thread 构造函数
        Thread thread1 = new Thread(task1);
        Thread thread2 = new Thread(task2);

        // 启动线程
        thread1.start();
        thread2.start();

        // 主线程逻辑
        System.out.println("Main thread is running...");
    }
}

并行和并发有什么区别

首先,并发主要是指在一个系统中能够管理多个任务,它们可以在同一个时间段内交替执行。也就是说,并发强调的是任务调度和切换——在单核CPU中,通过时间片轮转技术,使得多个线程或任务之间看起来像是同时在运行,实际上是在不断地切换。这种模式更侧重于如何合理安排任务的执行顺序、减少空闲时间和提高系统响应速度。

而并行则是指实际的同时执行。这个概念依赖于硬件资源,比如多核处理器。在多核环境下,不同的核心可以真正实现同时处理多个任务。并行更多描述的是物理上同时进行操作,而不是仅仅依靠快速切换来模拟“同时”运行。

从应用上来看,在Android开发中,我们经常会谈到并发,比如使用线程池管理后台任务、异步数据请求,甚至在UI操作中使用异步方式避免阻塞主线程。这时候,实际上很多时候我们是依靠并发来保证应用的响应性。而当设备有多核处理器时,这些并发任务也可以并行执行,从而提高执行效率,这时就同时具备了并行和并发的特点。

另一个角度是效率和资源利用的问题:

  • 并发主要目的是提高系统对多个任务的管理能力,它让程序在逻辑上可以同时处理多个请求,提升系统吞吐量;
  • 并行则更直接体现在性能上,通过利用多核优势,把任务真正划分到不同的计算核心上进行同时计算,可以显著加快处理速度。

解释一下哈希算法

“哈希算法是一种将任意长度的数据经过一定的算法处理后生成固定长度输出的算法。这个固定长度的输出,我们一般称之为‘哈希值’或者‘摘要’。哈希算法在计算机科学中有广泛的应用,比如在数据校验、密码存储、索引查找以及数字签名等场景都能看到它的身影。

首先,从原理上讲,哈希算法的核心是将输入的数据通过某种数学函数映射成一个固定长度的字符串。这个过程是单向的,难以逆向推导出原始数据,也就是说给定哈希值,恢复出原数据是非常困难的。这一点在密码学应用中非常关键,因为它能确保数据在传输中的完整性和安全性。

其次,哈希算法设计时通常会考虑几个重要特性:

  1. 确定性:对于相同的输入,每次计算出的哈希值都是一致的。
  2. 快速计算:一个好的哈希算法应当能够在较短的时间内处理大量数据,生成结果快速。
  3. 抗碰撞性:理想情况下,两个不同的输入数据经过同一个哈希算法后生成相同哈希值的概率应当非常低,这样可以防止碰撞,从而为数据校验提供一定保障。
  4. 雪崩效应:即输入的微小变化都会导致输出结果发生较大变化,这个特性有助于使哈希函数在数据散列方面更加均匀,避免聚集在某些特定值上。

在Android开发中,我经常使用哈希算法来做数据校验,比如校验下载的图片或文件是否被篡改过,也会在缓存中用哈希值作为键,快速定位资源请求时对应的缓存数据。另外,对于密码存储来说,安全哈希算法(比如SHA-256)结合加盐机制,是防止明文密码泄露的常见手段。通过保证哈希算法计算出的值与原始数据存在较强的映射关系,安全策略得到了有效加强。

总结来说,哈希算法的本质是一种映射技术,通过对数据的规范化处理生成固定长度的摘要,既保证了计算效率,又能提供一定程度上的安全性和数据完整性验证。

说说二叉树是什么,在哪里会用到树这个数据结构,为什么会用树这个数据结构

“二叉树其实是一种特殊的树形数据结构,每个节点最多只有两个子节点,通常被称为左子节点和右子节点。和其他树结构相比,二叉树的形式更为固定,这使得相关算法和操作更简洁高效,例如递归遍历、查找、插入和删除操作都有比较固定的实现思路。

在实际工作中,我经常接触到树形数据结构,不仅限于二叉树,还有B树、红黑树等。例如,Android开发中在UI组件层次结构、布局树的管理上都会用到树型结构,实际应用上我们通过树来描述页面元素之间的父子关系;此外,很多搜索和排序算法,比如二叉搜索树,为数据查找提供了一种高效的方案。这种数据结构最大的优势在于它能将大数据量问题分解成若干较小的子问题,在处理、分类以及搜索时能显著降低时间复杂度和空间浪费。

用树这种数据结构,首先在于它天然的层次化特点。树能够帮助我们把问题分层处理,无论是在表达复杂的逻辑关系、构建决策树还是进行优先级调度中,都能通过树的分支结构轻松展现出层级关系和依赖关系。就比如说,Android中的View树其实就是通过树的结构来控制渲染、事件分发和缓存的,这样能在降低复杂度的同时,提供高效的查找和遍历能力。

另外,树结构还能提高数据操作的效率。比如在二叉搜索树中,插入、删除、查找的时间复杂度平均为O(log n),这远远优于线性结构的数据处理;在一些需要快速定位和排序的场景中(例如文件系统、数据库索引),树结构能够提供更快的响应速度。因此,选择树结构解决问题往往是出于对效率和结构清晰性的双重需求。

总结来说,我认为二叉树作为树结构的一种简单且高效的形式,它在很多算法和实际应用中都有广泛的应用,比如UI元素管理、数据库索引和搜索操作。使用树这种数据结构关键在于它能够直观地表达层次关系,使得复杂问题得以分解,并且在数据查找上提供出色的效率,这些都是我们在系统设计和实际开发中非常看重的点。”

输入URL到渲染整个界面的一个过程,然后中间用了什么协议

首先,当URL输入后,最初浏览器会解析这个URL,分解为协议、域名、路径等部分。这个过程中确定了使用的协议,比如HTTP或HTTPS。一般来说,现在大部分情况都是使用HTTPS来保证数据传输的安全性,这就会涉及到TLS/SSL层的安全协商。

接下来,浏览器会进行DNS解析,将域名解析为服务器对应的IP地址。这一步是通过DNS协议完成的。DNS协议虽然和HTTP本身无关,但是它是成功连接服务器的基础。完成解析后,浏览器就知道目标服务器的IP地址了。

接下来,由浏览器发起TCP连接,该过程使用的是TCP协议。我们通常会说是三次握手建立连接,这一步的目的是确保双方都有能力相互通信,并建立起稳定的数据传输通道。在这个TCP连接建立之后,浏览器会根据URL中的协议发起HTTP请求。

HTTP协议是整个页面请求的核心协议。浏览器构造一个HTTP请求报文,这其中包含了请求方法(GET、POST等)、请求头信息(比如Cookie、User-Agent、Accept等)和可能的请求体。然后通过之前建立的TCP连接,将这个请求发到服务器。

服务器接收到HTTP请求后,会处理请求,比如从数据库中取数据、调用业务逻辑、读取静态页面资源等。经过一系列操作后,服务器构造一个HTTP响应报文返回给客户端。响应报文里包括状态码、响应头和响应体。响应体可能是一份HTML、CSS、JavaScript以及图片等资源。

浏览器拿到这个响应后,会开始解析HTML文档。HTML解析器将文档解析为DOM树,同时发现引用的附属文件,比如CSS样式、JavaScript脚本以及图片等资源。对于这些资源,浏览器会分别发起新的HTTP请求,这时候为了提高效率,往往会使用并发请求、缓存机制甚至是一些HTTP/2的多路复用机制来提高加载速度。“DOM”是文档对象模型(Document Object Model)的缩写,它是一种用来表示HTML、XML文档的编程接口。DOM将HTML或XML文档表示为一个树状结构,其中每个节点代表文档中的一部分(如标签、属性、文本等)。这使得开发者能够以编程方式操作文档的内容和结构。

在HTTP响应处理过程中,如果是HTTPS,还会经历TLS/SSL的解密,而HTTPS本身就是在HTTP协议基础上通过加密方式传输确保数据的机密性和完整性。浏览器在解析了HTML、CSS后,将构建渲染树。这时会计算出页面的布局,通过CSS的规则和JavaScript的逻辑,最终将页面绘制出来,也就是渲染出最终呈现给用户的界面。

总体来说,这个过程当中用到的核心协议主要包括:

  1. DNS 协议:进行域名解析,将域名转换为服务器的IP地址。
  2. TCP 协议:在客户端和服务器之间建立稳定的连接,主要使用三次握手来确认连接状态。
  3. HTTP/HTTPS 协议:在建立好TCP连接之后,基于HTTP协议进行数据传输,如果使用HTTPS还会在此之上使用TLS/SSL协议来加密通信。

讲讲HTTP状态码

“HTTP状态码其实是服务器在响应HTTP请求时返回的数字代码,用来表明请求的处理结果。服务器根据不同情况返回不同的状态码,这样客户端(例如浏览器或其他HTTP客户端)就能根据状态码知道请求是否成功以及出现了什么样的问题。在我的理解中,HTTP状态码主要可以分为五大类,每一类都代表着一种意义。

首先是1xx系列,也就是信息类状态码。这个系列的状态码主要用来表示请求已经被接收并且正在处理,但还没有最终的响应结果。例如,‘100 Continue’就表示客户端可以继续发送请求的剩余部分。不过这类状态码在实际开发中使用比较少,通常更多见于底层协议处理。

接下来是2xx系列,也就是表示成功的状态码。最常见的莫过于‘200 OK’,这个状态码说明请求已经成功,并且服务器已经返回了正确的响应。还有像‘201 Created’,这说明请求导致服务器创建了新的资源。对于客户端来说,2xx状态码意味着一切都运行正常,所以在开发过程中我们会着重关注2xx响应的正确解析和处理。

第三类是3xx系列,代表重定向。当服务器需要客户端采取进一步操作时就会返回这类状态码。比如‘301 Moved Permanently’表示资源已经被永久转移到另外的URL;‘302 Found’(有时也称为临时重定向)则提示客户端资源临时位于其它位置。重定向通常用在URL资源变更、或者处理缓存等场景,在实际开发中,理解3xx状态码对于优化用户体验和维护SEO都有帮助。

接下来是4xx系列,也就是客户端错误。这个系列的状态码表明客户端提交的请求存在某些问题,比如格式错误、参数缺失或者没有权限访问。常见的比如‘400 Bad Request’,就是请求不符合服务器要求;‘401 Unauthorized’则表示未进行身份认证或者认证失败;‘403 Forbidden’说明即使身份认证通过,也没有权限访问该资源;最常见的‘404 Not Found’表示请求的资源不存在。4xx状态码在调试和错误处理时非常关键,它帮助我们快速定位问题出在客户端的请求上。

最后是5xx系列,也就是服务器错误。当服务器无法处理看似合法的请求时,就会返回5xx错误。比如‘500 Internal Server Error’表示服务器内部出现了未处理的异常;‘502 Bad Gateway’通常出现在网关或代理服务器从上游服务器接收到无效响应时;‘503 Service Unavailable’则说明服务器暂时无法处理请求,可能是由于过载或者维护等原因。5xx状态码往往指示着后端服务出现故障,因此在实际工作中,我们需要结合日志和监控系统来快速定位和排查原因。

在Android开发中,我关注HTTP状态码的主要原因之一是网络通信。无论我们是使用OkHttp、Retrofit还是其他网络库,状态码都是我们判断请求成功与否和后续处理的重要依据。通过HTTP状态码,我们可以设计出友好的错误处理逻辑,比如在遇到4xx错误时提醒用户检查输入信息,而出现5xx错误时则提示稍后重试。同时,合理的状态码处理也有助于进行接口测试和服务端的调试,提高整个系统的健壮性。

进程的私有资源有哪些

“一个进程在操作系统上运行时,它会拥有一套自己的私有资源,这些资源主要是为了保证进程间的隔离性,提高系统的安全性和稳定性。这里我详细谈谈我对进程私有资源的理解。

首先,最直观也最重要的是进程的虚拟地址空间。每个进程都有自己的独立地址空间,这意味着它拥有独立的一块内存,其他进程无法直接访问这块内存。这块虚拟内存通常又分为几个区域:

  1. 代码段(Text Segment):存放程序的指令,是只读的,避免被意外修改。
  2. 数据段(Data Segment):用于存放初始化的全局变量和静态变量。
  3. BSS段:用于存放未初始化的数据,也是全局变量的一部分,启动时会被系统初始化为0。
  4. 堆(Heap):用于动态分配内存,随着进程的运行动态增长和缩减。
  5. 栈(Stack):用于存储局部变量、函数调用信息、返回地址等,每个线程都有自己的栈。

除了内存之外,每个进程还拥有自己的内核资源。例如:

  • 文件描述符:每个进程有一组独立的文件描述符表,记录着进程打开的文件、网络连接等资源,不同进程之间的文件描述符也是互相独立的。
  • 信号处理器:进程定义自己对某些信号的响应机制,这些信号处理设置也是进程私有的。
  • 进程控制块(PCB):这是操作系统内核为管理进程而维护的数据结构,记录了进程的状态、寄存器、优先级、父子关系、调度信息等信息,这些都只属于该进程。
  • 环境变量和命令行参数:这些也是进程启动时独有的,从外部环境传递到进程,用于初始化进程的运行状态。

在Android中,每个应用程序通常会运行在自己的进程内,所以这些私有资源直接体现在应用的生命周期、内存管理以及安全机制中。例如,Android应用的Dalvik/ART虚拟机运行在独立的进程中,这就保护了应用之间的数据和执行环境,防止了因一个应用错误或崩溃而影响系统中其他应用。

特性虚拟地址物理地址
访问范围每个进程独立且隔离,其他进程无法直接访问系统共享,但普通进程无法直接访问
访问权限由操作系统通过页表和权限管理控制只有内核态代码或受控情况下才能直接访问
隔离性非常高,确保进程间的独立性无隔离性,物理地址是全局资源
共享机制通过共享内存机制实现特定虚拟地址的共享通过映射实现多个虚拟地址对应同一物理地址

进程的调度方式,线程的调度方式

首先讲进程调度。进程是操作系统分配资源的基本单位,系统中的每个进程都有自己独立的虚拟地址空间和一组私有资源,调度的时候主要关注的是不同进程之间的公平性和系统整体效率。常见的进程调度算法有:

  1. 时间片轮转(Round Robin):这是很多通用操作系统采用的方案。每个进程分配一个时间片,当时间片用完时就会被挂起,操作系统切换到下一个进程。这样确保了每个进程都有一定时间得到执行,适合系统中进程数量较多的场景。

  2. 优先级调度:操作系统可以给每个进程设置一个优先级,在调度时优先选择优先级高的进程。优先级可以是静态设置的,也可以根据进程的行为动态调整。这种方式可以确保关键任务得到及时处理,但有时也会出现低优先级进程饿死的问题,因此需要设计升降优先级的机制。

  3. 多级反馈队列:这是一种结合了时间片和优先级的方法。系统通过多个队列,每个队列对应不同的优先级和时间片配置。进程如果没能在规定时间片内完成就会被降级到低优先级队列中,这样既保证了短任务的快速响应,又防止了长任务独占CPU资源。

此外,在具体实现上,许多现代操作系统(比如基于Linux内核的Android系统)使用的都是抢占式调度,即在任何时刻,如果有一个更高优先级的进程准备好运行,操作系统会中断当前进程,把CPU分配给那个高优先级的进程。这种调度方式能更好地响应实时性要求和系统中突发的高优先级任务。

接下来聊聊线程的调度。线程是程序执行的基本单位,一个进程内部的多个线程共享进程的部分资源(如地址空间),但是每个线程都有独立的执行栈和程序计数器。当涉及到多线程调度时,虽然概念上跟进程调度类似,但是区别在于线程的调度通常是在同一进程内部实现相对轻量级的切换。常见的调度方面主要有:

  1. 内核级线程和用户级线程:在一些操作系统中,线程调度分成两种模型。内核级线程由内核直接管理调度,而用户级线程则由线程库在用户空间管理调度。像Android这种操作系统主要使用内核级线程,通过Linux内核的调度器来保证线程间的公平性和实时响应。

  2. 线程调度算法往往和进程调度类似,也包括时间片轮转和基于优先级的预占式调度。由于线程本身比进程的结构更轻量,切换开销更低,所以在多线程场景下,系统可以更频繁地切换线程以提高响应性。

  3. 除了操作系统层面的调度外,在应用层我们也会对线程调度进行一些控制。例如,在Android中,我们经常使用线程池管理异步任务,合理设置线程池的大小和队列,从而保证后台任务不会因为线程上下文切换频繁而影响整体性能。应用层的调度策略也会涉及任务优先级设定、任务等待与唤醒机制等,这需要结合具体的业务场景来设计。

总的来看,无论是进程还是线程的调度,核心都是如何高效、合理地利用系统的CPU资源,同时平衡系统各个任务的响应时间和整体吞吐量。进程调度侧重于进程之间的资源分配和隔离,而线程调度则更多关注于进程内部任务的并发执行。

设计一个图片库APP,实现从服务端下载图片,以及本地缓存的实现

“在设计一个图片库APP、实现从服务端下载图片以及本地缓存的过程中,我主要会从以下几个方面来考虑和设计。

首先,在整体架构上,我会将这个应用划分为三层:数据层、业务层和展示层。数据层主要负责与服务端进行通信、处理网络请求以及管理本地缓存;业务层则处理图像的生命周期、下载逻辑、缓存策略以及错误处理;而展示层负责将图片展现给用户,同时确保UI的流畅性,比如使用异步加载和显示占位图。

  1. 服务端下载流程: 我会使用成熟的HTTP网络库,比如OkHttp或Retrofit来发起图片下载请求,通常采用HTTP的GET请求。考虑到图片下载可能会涉及到较大的数据传输,通常还会支持断点续传以及重试机制。此外,我会考虑到网络不稳定、超时等情况,设置合适的超时时间和错误处理机制,确保请求过程有较好的可靠性。

  2. 本地缓存方案: 本地缓存对图片加载来说非常重要,因为它能大幅提高用户体验和减少网络请求。我的设计会包括两层缓存:

    a. 内存缓存:利用LruCache这类工具缓存图片对象,能够在下次利用这些图片时迅速返回。内存缓存主要适合短期、快速访问的场景,缺点是内存有限,所以设置合适的缓存大小,防止内存溢出。

    b. 磁盘缓存:针对长时间缓存,我会使用磁盘缓存,比如通过DiskLruCache来实现。磁盘缓存可以存储下载过的图片文件甚至经过压缩处理后的文件,以便重新加载时能够避免重复的网络请求。为了保证磁盘IO的效率,图片下载完成后可以异步写入磁盘缓存,而读取时同样采用子线程处理,防止在主线程中进行磁盘操作而引起卡顿。

    设计中,我们一般都会先检查内存缓存,如果未命中,再去磁盘缓存查找,如果依然没有,再发起网络下载。这样层层递进的缓存机制既能提高加载速度,也能尽可能降低网络资源浪费。

  3. 缓存数据的一致性以及清理策略: 缓存管理还需要考虑到存储空间的限制和缓存数据的生命周期。一方面,可以设置磁盘缓存的大小上限,比如几百兆的空间,并定期清理较长时间未访问的缓存。另外,针对图片的版本可能变化的情况,可以在请求图片时带上版本号或者ETag等信息,当服务器返回不同版本时覆盖本地缓存。内存缓存则会自动根据LRU策略淘汰较长时间未使用的对象。

  4. 异步加载和线程调度: 由于网络请求和磁盘操作都是比较耗时的操作,在业务层我会使用线程池、异步任务或者RxJava等来管理线程调度,确保这些操作不会阻塞主线程。同时,图片加载完成后,通过主线程回调更新UI,这样既能保证操作的高效执行,又能确保UI流畅。

  5. 边界条件及用户体验优化: 除了基本的下载与缓存之外,还考虑到图片加载过程中的异常处理和占位图展示,比如在网络不可用或者图片加载失败时,能够提供默认占位图片;当遇到大图时,可以在下载和展示时进行适当的压缩或缩略图生成,提升加载速度和内存利用率。此外,在图片滚动列表中,我们也可以对加载图片进行节流和预加载优化,避免因页面滚动过快而造成大量图片同时加载的情况。

使用HTTP协议传输一个10兆的文件,中途断了,如何实现断点重连

“首先,针对一个10兆文件的下载中途断开的问题,我们通常采用的是HTTP协议中提供的断点续传机制来解决,也就是利用HTTP的Range请求头来实现断点重连。“Range请求头”是HTTP协议中的一个请求头字段,它允许客户端向服务器请求某一部分的资源,而不是请求整个资源文件。这种机制被称为HTTP范围请求(HTTP Range Requests)。

断点续传的基本思路如下:当文件下载过程中断了,我们需要在客户端记录已经成功下载的数据的长度,也就是已经接收的字节数。当恢复下载时,可以在新的HTTP请求中添加一个Range头,请求服务器从上次结束的位置继续传输后续数据。服务器在收到这样的请求后,如果支持断点续传,就会返回206 Partial Content响应,并仅返回指定范围内的数据。这样客户端只需要将新下载的数据追加到已有文件上面即可完成整个文件的下载。

在具体实现中,我们首先要确保服务器端支持断点续传,即服务器是否允许并正确响应Range请求。一般来说,大部分标准的HTTP服务器都支持这个特性。客户端在发起初次请求时,会先检查响应头中是否包含Accept-Ranges标识。如果允许续传,则在下载过程中应当记录中断时的字节偏移量。当网络中断或者异常发生后,我们可以通过读取之前保存的偏移量来构造续传请求。

比如在Android中文件下载场景,我们会通过一个网络库或者HTTP客户端库,构造一个包含Range头请求的HTTP GET请求。这个Range头格式类似于‘Range: bytes=<已下载的字节数>-’,告诉服务器从这个偏移量开始返回剩余数据。服务器返回206响应后,我们用输出流将新数据追加到已有的文件中,这样最终重新拼接成完整的文件。

除此之外,为了提高系统的健壮性,我们还需要设计一些重试机制,确保在各种网络不稳定或者服务器响应异常的情形下能够正常启动重连。同时,我们也会对下载过程中的数据进行校验,比如使用哈希值验证最终文件的完整性,确保数据没有在断点续传的过程中出错或者丢失。

手写一个单例模式

public class Singleton {

    // 1. 私有静态变量,用于存储唯一实例
    private static volatile Singleton instance;

    // 2. 私有构造函数,防止外部直接创建实例
    private Singleton() {
        // 防止通过反射破坏单例
        if (instance != null) {
            throw new RuntimeException("Use getInstance() method to create");
        }
    }

    // 3. 提供公共的静态方法获取单例实例
    public static Singleton getInstance() {
        if (instance == null) { // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) { // 第二次检查
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

讲讲AQS

AQS 是 AbstractQueuedSynchronizer 的缩写,是 Java 并发包(java.util.concurrent)中的一个核心类,位于 java.util.concurrent.locks 包中。它是实现锁(Lock)和同步器(Synchronizer)的基础框架。

  1. 为什么要有 AQS

    • 在多线程环境里,我们常常需要一个通用的、可复用的“排队+阻塞/唤醒”机制,来实现互斥锁、读写锁、信号量、倒计时闩锁等。
    • 如果每个同步组件都自己写一套队列和阻塞逻辑,代码会非常冗余而且容易出错。AQS 提供了一套模板:管理一个整型状态 state、维护一个 CLH(Craig–Landin–Hagersten)双向队列,当线程获取失败时入队阻塞,释放时逐条唤醒。
  2. AQS 的核心数据结构

    • state(int):表示同步器的当前状态,比如对 ReentrantLock 来说,state 记录锁重入次数;对 Semaphore 来说,state 记录可用许可数。【Reentrant 的意思是“可重入”,表示同一个线程可以多次获取同一把锁,而不会发生死锁。】(锁会被“自己卡住”的情况,实际上指的是 线程由于不支持可重入性而导致的死锁。如果一个锁不支持可重入性,当某个线程再次尝试获取已经由自己持有的锁时,就会被阻塞,形成自我死锁。)
    • Node 双向链表:CLH 队列,头尾各有指针 head、tail。每个 Node 代表一个请求锁(或许可)的线程,包含 waitStatus、prev/next、thread 等字段。
    • waitStatus:标记节点状态,比如 SIGNAL(等待被唤醒)、CANCELLED(放弃获取)、CONDITION(在 Condition 队列)等。
  3. 获取与释放流程(以独占模式为例)

    • 获取:调用 AQS 的 acquire() 系列方法。首先快路:tryAcquire(),如果当前 state 满足条件(比如 state == 0),用 CAS 把 state 设置为 1,并把自己设置为 owner,直接返回。
    • 如果 CAS 失败或 state 不满足,就会把当前线程封装成 Node,插到同步队列尾部,然后调用 park() 阻塞自己。
    • 释放:调用 release() 系列方法。先用 tryRelease() 逻辑去更新 state(比如减 1),如果返回 true,说明锁真正释放了,就会唤醒队列 head 的下一个节点的线程(unpark)。
  4. 独占模式 vs 共享模式

    • 独占(Exclusive):只有一个线程能持有锁,比如 ReentrantLock。Node 的模式是 EXCLUSIVE。
    • 共享(Shared):允许多个线程同时持有资源,比如 Semaphore、CountDownLatch(倒计时结束后所有等待线程都能同时通过)。AQS 提供 acquireShared()/releaseShared(),在释放时会 unparkAll 或者按顺序唤醒后继线程。
  5. Condition 支持

    • AQS 还支持 Condition(条件队列)。Condition 本质上是另一张队列,线程调用 await() 时,会先把自己从同步队列移到 condition 队列并 park(),signal() 时,再把它转到同步队列尾部,等到前面的线程释放锁时,再重新竞争。
  6. 为什么性能好且可靠

    • CLH 队列:非阻塞地用 CAS 操作 head/tail;线程阻塞由 LockSupport.park()/unpark() 提供,不会忙等。
    • 模板方法设计:只需子类实现 tryAcquire/tryRelease/tryAcquireShared/tryReleaseShared 几个方法,就能自动获得队列管理、排队、阻塞唤醒等完整逻辑。
  7. 源码学习后的收获

    • 明白 state 的语义一定要和子类互相信任、配合,这是整个同步器的“核心开关”。
    • 入队、出队的边界条件、CAS 失败的重试逻辑、不允许乱序释放/获取,都是并发高可用的关键。
    • Condition 的实现很巧妙:先在 condition 队列 park,再 signal 时跨队列搬运,保证了 await/signal 的先后与锁的正确释放。
  8. 在 Android 项目中的实际价值

    • 我在项目里自己基于 AQS 实现过一个“限流”/“排它信号量”组件,只要实现 tryAcquireShared() 去判断当前并发数,就可复用 AQS 的排队与唤醒。
    • 避免自己写一遍线程队列和阻塞唤醒机制,减少 bug,也让性能和 JDK 自带的同步器保持一致。

总结:AQS 是一套通用的“状态+FIFO 队列+阻塞唤醒”框架,它把复杂的多线程排队逻辑都模板化了。

HTTP状态码302

HTTP 状态码 302 是一种重定向状态码,表示客户端的请求资源暂时被移动到了另一个位置。服务器通过响应头中的 Location 字段告知客户端新的 URL,客户端需要根据这个新的地址重新发起请求。


1. 302 的核心含义

  • 暂时性重定向:302 表示资源的临时移动,客户端在接收到这个响应后会访问新的地址,但下次请求时仍然可以继续使用原始地址。
  • 通常用于临时调整资源的访问路径,比如负载均衡、A/B 测试、流量分流等场景。
  • 与 301(永久重定向)不同,302 不会要求客户端更新缓存中的地址。

2. 302 的常见使用场景

  • 用户认证和重定向到登录页面
    • 比如用户访问受保护的资源,但尚未登录,服务器会返回 302 状态码,将用户重定向到登录页面。
  • URL 的临时变更
    • 当某个资源的访问路径暂时改变时,可以使用 302 指向新的路径。
  • A/B 测试
    • 将不同用户随机分配到不同版本的页面,以便测试页面效果。
  • 负载均衡
    • 基于业务需求,将用户请求重定向到不同的服务器。

3. 302 状态码的工作流程

  1. 客户端发起请求

    • 用户访问某个 URL,浏览器(客户端)向服务器发送请求。
  2. 服务器返回 302 响应

    • 服务器返回 302 状态码,并在响应头中包含 Location 字段,指向新的 URL。
  3. 客户端自动重定向

    • 浏览器根据 Location 字段的地址,自动向新的 URL 发起请求。
  4. 完成重定向

    • 客户端最终获取到新的资源。

4. 302 和其他重定向状态码的区别

  • 301(永久重定向)
    • 表示资源已经被永久移动到新的位置,客户端和搜索引擎应该更新其缓存地址。
  • 302(临时重定向)
    • 资源仅仅是暂时移动,客户端下次请求时仍然可以使用原始地址。
  • 303(See Other)
    • 表示资源可以在另一个 URL 通过 GET 方法获取,通常用于 POST 请求后的重定向。
  • 307(临时重定向)
    • 类似于 302,但要求客户端在重定向时使用相同的 HTTP 方法(比如 POST 请求不会变成 GET 请求)。

HTTPS如何传输数据

  1. TLS 握手阶段

    • 客户端(Android App)发起 HTTPS 请求时,首先和服务端进行 TLS(或说 SSL)握手。
    • 客户端发送 ClientHello,里面带了支持的协议版本(比如 TLS 1.2、1.3)、加密套件列表(对称加密算法、非对称算法、消息认证算法)、随机数等。
    • 服务端收到后返回 ServerHello,选择一个双方都支持的加密套件、协议版本,以及自己的随机数和 X.509 证书。
    • 客户端拿到证书后,会用内置或系统信任的根 CA 列表校验证书合法性,包括域名、有效期、签名链等。这一步保证了客户端确实在和正确的服务器通信,没有被中间人篡改。
  2. 密钥交换与会话密钥生成

    • 在传统的 TLS 1.2 里,基于服务端证书的公钥,客户端会生成一个预主密钥(pre-master secret),用服务端公钥加密后发给服务端。
    • 服务端用私钥解密得到预主密钥。
    • 双方各自利用 ClientHello 和 ServerHello 中的随机数,以及这个预主密钥,按约定的伪随机函数(PRF)生成对称会话密钥:一个用于加密,一个用于消息认证。
    • 在 TLS 1.3 中,引入了更高效的椭圆曲线 Diffie–Hellman(ECDHE)模式,直接完成密钥交换并生成会话密钥,同时改进了握手性能和前向安全性。
  3. 对称加密与消息完整性

    • 一旦握手完成,客户端和服务端就进入 Record 层,用刚才协商好的对称加密算法(AES-GCM、ChaCha20-Poly1305 等)对后续的 HTTP 报文进行加密和认证。
    • 每个加密报文都附带一个消息认证码(MAC)或 AEAD 标记,确保数据在传输过程中不可被篡改,也能防止重放攻击。
  4. HTTP 请求和响应交互

    • 加密通道建立后,客户端发起典型的 HTTP 请求头和请求体(比如 GET、POST)。这些报文在 Record 层被加密打包,经过 TCP 发送到服务器。
    • 服务端解密后处理业务逻辑,再把 HTTP 响应结果同样加密后发回客户端。
    • Android 端常用 OkHttp、HttpURLConnection、Volley 等网络库,本质都是在 TLS Record 之上封装 HTTP 逻辑,开发者只需关注高层 API。
  5. 会话重用与性能优化

    • 为了避免每次都全量握手,TLS 支持 Session ID 或 Session Ticket 机制,客户端第一次握手后缓存 Session 信息,下次访问同一域名就可以用“0‑RTT”或快速恢复握手,省去全握手的延迟。
    • Android 平台上,OkHttp 默认开启 Connection Pool,底层也会复用 TCP + TLS 连接,进一步减少延迟。
  6. 安全扩展与证书校验

    • 对于更严格的安全需求,Android 端可以做公钥固定(Pinning)、自定义 TrustManager、证书透明度检查(Certificate Transparency)等,进一步防止中间人或伪造证书。
    • 在最新 Android 版本中,Network Security Configuration 允许在资源文件中声明信任的证书和域名策略,管理更灵活。

让你设计一个牛客网系统,你会考虑什么,如何实现代码编译

一、整体功能与模块拆分

  1. 用户与权限
    • 账号体系:注册/登录(支持手机、邮箱、第三方)
    • 权限分层:普通用户、课程讲师、管理员、评测节点角色
  2. 题库管理
    • 题目维度:选择题、编程题、数据库题、SQL 题
    • 标签分类、难度分层、测试用例管理、题解与讨论
  3. 提交与评测流水
    • 提交记录:时间、语言、代码快照、测试用例覆盖、结果统计
    • 排行榜/成绩册:单题榜、考试榜、日常练习榜
  4. 竞赛/课程系统
    • 实时赛与赛后编译、课程作业、批改流程
  5. 社区与学习
    • 讨论区、题解分享、笔记、消息推送

非功能需求(最关键)

  • 高并发:评测节点峰值能撑住上万次并发提交
  • 安全隔离:防注入、拒绝服务、单用户代码互不干扰
  • 可扩展:评测机可随流量横向扩容
  • 高可用:评测队列与存储冗余,后台服务无单点

二、系统架构(高层)

  1. 前端与 API 层
    • Web/移动端(Android)通过统一 API 网关,做限流、鉴权、路由
  2. 业务服务层
    • 用户服务、题库服务、提交服务、成绩服务、社交服务
  3. 存储层
    • 关系型数据库(题目、用户、提交元数据)
    • 对象存储(代码快照、测试数据)
    • 缓存(Redis:排行榜、热题、会话)
  4. 消息队列
    • RabbitMQ/Kafka:提交请求进评测队列
  5. 评测集群(Judge Cluster)
    • 多台评测节点(容器或微VM),按提交异步拉取、执行、回收
    • 节点自动注册/发现,健康检查、弹性扩缩
  6. 日志与监控
    • Prometheus + Grafana 监控评测延迟、失败率、资源使用
    • ElasticSearch/Kibana 做日志分析、安全审计

三、代码编译与评测模块实现思路

  1. 提交入队

    • 用户在前端提交代码,API 层做基础校验(大小、语言白名单)
    • 元数据写入 MySQL,代码持久化到对象存储,发布一条消息到评测队列
  2. 评测节点拉取执行

    • 节点进程或服务不断从队列消费任务
    • 每个任务包含:代码存储路径、语言、测试用例列表、时间/内存限额
  3. 隔离执行环境

    • 每次评测在轻量级隔离容器(Docker 或 gVisor)/微VM(Firecracker)中运行
    • 限制 CPU 核心、内存、磁盘写入、网络访问(Sandbox)
    • 从宿主机或镜像中挂载编译工具链:GCC、Java SDK、Kotlin 编译器、Python 解释器 等
  4. 编译阶段

    • 根据语言选择编译命令:
      • C/C++:gcc/clang 编译输出可执行文件
      • Java/Kotlin:javac/kotlinc 生成 .class 或 .jar
      • Python/脚本:可跳过编译,直接进入执行
    • 捕获编译日志、错误码,若编译失败,直接返回 “编译错误”
  5. 运行与测试

    • 对每个测试用例:
      1. 以时限、内存限额启动可执行程序或 JVM,输入用例数据
      2. 读取标准输出,与标准答案比对(支持精确匹配、忽略空格、正则对比)
      3. 收集运行时间、内存峰值、返回码,做超时/内存超限检查
    • 并行化:一个提交的不同用例可多线程并行跑,也可批量限制并发数
  6. 结果聚合与反馈

    • 汇总所有用例执行结果:AC、WA、TLE、MLE、RE
    • 写回业务库,更新提交状态、排行榜、用户做题记录
    • 完整日志(编译日志、运行日志)存入对象存储,链接下发给前端
  7. 性能和可扩展性

    • 容器预热:评测节点常驻编译容器池,减少启动延迟
    • 队列并发控制:按课程或比赛限流,优先级队列保证实时赛请求优先
    • 状态缓存:常用语言镜像和编译工具链缓存到内存或本地文件系统
    • 多集群部署:不同地域/可用区部署,靠近用户降低网络延迟
  8. 安全和隔离

    • 禁用网络访问或仅允许拉取黑名单外资源
    • 文件系统挂载只读或限制写到沙箱目录
    • 审计执行进程,定期清理无效容器与临时文件

四、在 Android 客户端的体现

  1. 状态展示:异步拉取提交状态、排行榜,用 RecyclerView 优雅展示
  2. 断点续传:提交大代码或附件可中断重试
  3. 实时推送:通过 WebSocket 或 MQ-Push 机制,及时更新评测进度
  4. 安全校验:客户端做二次校验,防止恶意提交超大文件或非法语言

死锁条件,然后如何破坏死锁

一、死锁的四个必要条件

  1. 互斥(Mutual Exclusion)
    • 至少有一个资源必须以不可共享的方式被占用。
    • 例如,一个对象锁(synchronized 或 ReentrantLock)一次只能被一个线程持有。
  2. 占有且等待(Hold and Wait)
    • 线程至少持有一个资源,并且在等待获取其他正在被别的线程占用的资源时,不释放自己已占有的资源。
  3. 不可剥夺(No Preemption)
    • 已分配给线程的资源,在线程使用完之前,不能强制从它手里剥夺。
    • 在 Java 的锁模型里,锁持有者不会被系统强行抢占,必须等到它主动释放。
  4. 循环等待(Circular Wait)
    • 存在一个线程集合 T1→T2→…→Tn→T1,Ti 等待 Ti+1 占有的资源,形成环路。

只有当这四个条件同时满足时,才会真正发生死锁。

二、如何破坏或预防死锁
要破坏死锁,就要设计上刻意打破上面任意一个条件。这些策略可分为“预防/避免”与“检测/恢复”两大类。

  1. 破坏“占有且等待”
    • 一次性申请所有资源:设计 API 时,让业务在进入关键区之前,一次性按固定顺序申请所需锁,拿不到就全部释放、稍后重试。
    • Lock.tryLock + 超时回退:使用 ReentrantLock.tryLock(timeout),如果在超时时间内拿不到,释放已持有的锁,等待随机或递增退避后再试。
    这样就不会长时间持有部分资源,减少形成循环等待的机会。

  2. 破坏“循环等待”
    • 全局加锁顺序:给每个锁分配一个全局编号,所有线程在申请多个锁时,必须按编号从小到大依次加锁,释放时再按相反顺序,这样就不可能出现环形依赖。
    • 细化锁粒度、减少嵌套锁:在项目中,尽量避免在一个锁内部再去获取另一个锁;如果确实要嵌套,先梳理好顺序,并用文档或代码注释约定。

  3. 破坏“不可剥夺”
    • Java 原生锁无法剥夺,但对自定义资源管理,可以引入“租约”机制:如果线程持有资源超时未释放,资源管理器可强制回收,并通知持有者。
    • 在业务层面,遇到长期卡住的操作要有监控,及时报警或做人工介入。

  4. 破坏“互斥”
    • 将资源设计成可重入或可共享的,只要业务允许,如无状态计算、读多写少场景可用读写锁(ReadWriteLock),让多个线程并行获取读锁。
    • 但可共享并非对所有场景都适用,这种方法适用于对性能要求更高的读场景。

三、死锁检测与恢复

  1. 监控与诊断
    • 在生产环境中,可以定期在 JVM 中触发 Thread.getThreadInfo(...) 或 JMX Bean,检测是否有多条线程都在 BLOCKED 状态,并且相互等待。
    • Android 上也可以通过 adb shell dumpsys activity 或采集 TraceView 跟踪锁竞争。

  2. 死锁恢复
    • 一旦监测到死锁,就需要把其中一个线程“踢出”循环,比如通过设置某个标志,让它强制退出等待,或者抛出异常回滚。
    • 对关键业务,要做好幂等/回滚设计,避免因为抢锁失败而导致数据不一致。

共享内存的缺陷

  1. 可见性(Visibility)问题
    我在项目里经常会遇到:一个线程修改了某个共享字段,另一个线程却看不到最新值。
    • 根据 Java 内存模型,普通字段的写操作不保证“及时”或“一致”地对其它线程可见,可能被缓存在线程各自的寄存器或 CPU 缓存里。
    • 这就导致读写顺序重排和“读到旧值”问题,比如我在后台线程更新状态标志后,主线程还一直在用老状态做判断,BUG 很隐蔽。
    • 解决办法是用 volatile、synchronized、AtomicXXX 或者显式的内存屏障,但每次加锁或加 volatile,都要考虑性能和正确性。

  2. 竞态条件(Race Condition)与原子性(Atomicity)
    当多个线程同时对同一块共享内存进行读–改–写操作时,非常容易出现竞态:
    • 比如一个简单的 counter++,实际上拆成“读—加—写”三步,不加锁肯定会丢计数。
    • 我在埋性能埋点时,就踩过这个坑,导致统计数据一直偏低。
    • 用 synchronized 或者 AtomicInteger 都能保证原子性,但也会带来额外的性能开销,特别是在高并发场景下,锁竞争就成了瓶颈。

  3. 死锁(Deadlock)和活锁(Livelock)
    • 为了保护共享内存,我们常会给多段代码加上互斥锁(synchronized、ReentrantLock 等),如果锁的获取顺序不一致,稍不留神就容易死锁。
    • 我们曾在一个模块里正准备关闭多个资源时,发现两个线程互相等待对方释放锁,线上服务直接卡住。
    • 即便是避免死锁,也要注意活锁:线程不停地让出锁,却永远抢不到执行机会。

  4. 性能开销与锁竞争
    • 任何形式的同步(锁、CAS 重试等)都会引入“停顿”——线程要么排队等待,要么自旋重试。
    • 在 Android 这种资源相对有限的环境下,UI 线程如果也要等待某个锁释放,就可能导致 ANR。
    • 我曾经把部分业务逻辑丢到 DefaultDispatcher,也发现当并发量大时,线程切换和锁竞争让整体吞吐量反而下降。

  5. 复杂性与可维护性
    • 共享内存意味着责任重叠:谁来保证读写顺序?谁来负责加锁、解锁?
    • 随着业务迭代,代码里会出现各种各样的同步块、条件等待、notify/notifyAll,逻辑变得臃肿而难以理解。
    • 我在 Code Review 时常常要花大量时间,去理清多个锁之间的依赖关系,稍不注意就会漏掉一个边界条件,埋下并发坑。

  6. 调试难度与重现成本
    • 并发缺陷往往是“有时出现、有时不出现”的,他可能只在线上高并发或特定机型才会复现。
    • 我们很难在本地环境或单线程模拟器上重现,然后就要借助严格的日志打点、线索还原才能定位。
    • 而一旦用户遇到闪退、数据不一致,往往要耗费很长时间才能排查到哪个共享变量、哪个代码路径没同步好。

  7. 高级问题:伪共享(False Sharing)
    • 在 CPU 缓存行级别,如果两个线程频繁修改相邻但不相关的共享字段,也可能因为“缓存行抖动”导致性能急剧下降。
    • 虽然在高级服务器端比较常见,在 Android 少见些,但如果我们把多个状态字段放在同一个对象里,就有可能遇到。

  8. Android 特有场景:跨进程共享
    • Android 的 Binder、SharedMemory、AIDL 等机制提供进程间共享内存,但会涉及到权限、安全、内存映射的生命周期管理。
    • 我们曾遇到进程重启导致 SharedMemory 区段被回收,另一端继续访问就崩溃;或者安全问题没做好,导致敏感数据泄露。

——

总结一下
在 Android/Java/Kotlin 里使用共享内存,必须非常谨慎。可见性、原子性、锁竞争、死锁、调试难度、代码可维护性……一环环都可能出问题。
我个人习惯是:

  1. 避免无谓共享,能用局部变量就别用共享状态;
  2. 对必要的共享变量,优先考虑 Kotlin 的 @Volatile + 线程安全数据结构,或使用协程的 ChannelMutexSharedFlow 等更高层次的并发模型;
  3. 对于真正需要跨线程通信的场景,尽量选用线程安全队列或消息机制,减少手写锁的使用;
  4. 加强测试:编写并发测试用例,借助 Thread.sleepCountDownLatch 等手段模拟极限场景。

进程的概念 状态 PCB

  1. 进程的概念
    “进程”可以看作是操作系统对正在执行的程序的抽象,是系统进行资源分配和调度的基本单位。它不仅仅是一段代码,还包含了程序执行时所需的:
  • 独立的地址空间:每个进程都有自己私有的虚拟内存,保证彼此之间的数据不被直接篡改。
  • 资源集合:包括打开的文件描述符、网络连接、内存映射、信号处理方式等。
  • 执行上下文:程序计数器(PC)、一组寄存器、堆栈指针等,记录了进程执行到哪一步、当前堆栈状况是什么。

在 Android 上,应用进程本质上也是 Linux 进程,都是由 zygote 预先初始化好核心库后 fork 出来的,这就体现了进程概念的复用和隔离。

  1. 进程的状态
    一个进程从创建到消亡,会经历若干个状态。主流教材里通常分为五种基本状态:
  • New(新建)
    刚由操作系统创建,还没准备好运行所需的全部资源,比如还没分配好内存空间、打开必要的文件。
  • Ready(就绪)
    已经完成了所有初始化,正在等待 CPU 调度。就绪队列里可能排着很多进程,操作系统根据调度算法(优先级、时间片轮转、CFS 等)来决定哪个进程马上运行。
  • Running(运行)
    进程已被分配到 CPU,正在执行指令。此时它能读写寄存器、访问内存、执行系统调用等。
  • Blocked / Waiting(阻塞/等待)
    当进程执行到需要等待某些事件(如 I/O 完成、信号到达、互斥锁释放)时,它会从运行态转到阻塞态,此时不会占用 CPU,直到条件满足才会回到就绪态。
  • Terminated(终止)
    进程执行完毕或被强制结束,进入清理阶段。操作系统会回收它占用的所有资源,然后在进程表中撤销它的信息。

在 Linux(也是 Android 的底层)中,还有一些细分状态,比如“可中断睡眠”(等待信号可被打断)或“不可中断睡眠”(用于关键底层操作),以及“挂起”状态等。但核心逻辑就是通过这些状态转换来管理并发和资源。

  1. PCB(进程控制块)
    进程控制块是操作系统维护进程状态的“档案”,在 Linux 内核里对应 task_struct 结构。它主要保存:
  • 标识信息
    • 进程 ID(PID)、父进程 ID(PPID)、用户/组 ID。
    • 进程名、命令行参数(方便调试、审计)。
  • 状态信息
    • 当前状态(Running、Ready、Sleeping…)。
    • 程序计数器 PC 和一组寄存器的值(上下文切换时要保存/恢复)。
  • 调度信息
    • 优先级、静态/动态优先级、时间片剩余值。
    • 调度策略(CFS、实时策略 SCHED_FIFO/SCHED_RR 等)。
  • 内存管理信息
    • 页表基址或 mm_struct 指针(管理虚拟地址到物理地址的映射)。
    • 堆栈指针、各个段的边界(code、data、heap、stack)。
  • 文件和 I/O 信息
    • 打开的文件描述符表(指向 file 对象的指针数组)。
    • I/O 设备或网络连接的状态。
  • 会计与统计信息
    • 已用 CPU 时间、用户态/内核态时间、上下文切换次数。
    • 内存使用量、打开文件数量等。
  • 进程间关系
    • 子进程链表、信号处理方式、管道/消息队列/共享内存等 IPC 信息。

操作系统通过维护每个进程的 PCB,把它们串成一个“双向链表”或多级队列。进程切换时,内核会:

  1. 保存当前运行进程的 PCB(把寄存器、PC 写进去),
  2. 选出下一个就绪进程,从它的 PCB 中恢复上下文(寄存器、PC),
  3. 切到它的地址空间,继续执行。

结合 Android 场景补充

  • Android 的 Zygote 进程在启动时就创建好了一个包含 Dalvik/ART 虚拟机、应用框架和核心库的进程镜像,后续 app 进程都是 fork 自 Zygote 的虚拟内存。这样做既保证了进程隔离,也极大地缩短了启动冷启动时间。
  • 每个 Android 应用的 ActivityManagerService、WindowManagerService 等,都能通过 Binder IPC 在进程控制块(task_struct)基础上管理进程的生命周期:前台进程、可见进程、服务进程、后台进程、空进程等状态,进而做内存回收和进程优先级调整。

总结

  • 进程是程序执行时的实体,是内存和资源的拥有者;
  • 通过就绪、运行、阻塞、终止等状态及其转换,OS 高效管理并发;
  • PCB(或 task_struct)承载了进程的全部关键信息,是进程切换和调度的核心。

TCP为什么不是对称的

1.角色分工不同

  • 在 TCP 里,始终存在一个“主动打开”(Active Open)的一端和一个“被动打开”(Passive Open)的一端:
    • 客户端发起 connect(),发送 SYN;
    • 服务器端在 listen() → accept() 阻塞,等待 SYN。
  • 这个设计本质上就不是对称的:客户端主动握手,服务器被动响应,两个角色的状态机和操作流程就不一样。

2.三次握手的不对称

  • 第一次:客户端只发 SYN,告诉对方“我想建连接,我从这个序号开始”;
  • 第二次:服务器回 SYN+ACK,既确认了客户端的序号,也附带自己的初始序号;
  • 第三次:客户端再发 ACK,确认服务器的序号。
  • 如果是完全对称的,理论上两端都要各自先后发送同样报文,但这样会造成 deadlock(双方同时等对方发包)。三次握手正是为了解决“先发还是先等”的问题,必须分出主次才可正确建立全双工会话。

3.四次挥手(断开)里的主动/被动关闭

  • TCP 断开连接时,又是对称(全双工)和不对称(顺序)同时存在:
    • 主动关闭方(调用 close() 的一端)先发 FIN,进入 FIN‑WAIT;
    • 被动关闭方收到后也发 FIN,再进入 TIME_WAIT 或 CLOSING;
    • 两端各自半关闭(half‑close)——自己不能再发数据,但还可以接收对方剩余的数据。
  • 更重要的是:TIME_WAIT 状态只出现在主动关闭的一方,用来确保老报文不会“误连接”到新会话。这个角色分配也体现了不对称性。

4.序列号、确认号、窗口管理各自维护

  • TCP 是全双工的,但每个方向的流量控制(receive window)、拥塞控制(slow start、congestion window)都是独立维护的。
  • 两端会记录不同的 ACK 进度、不同的拥塞状态和窗口大小,它们并不是镜像复制,而是各自根据网络条件单向调整。

5.设计取舍带来的不对称

  • 避免僵持:如果两端都必须先等待对方,连接就永远建不起来;
  • 老报文保护:TIME_WAIT 只放在主动端,避免大多数场景下的“旧报文入侵”;
  • 性能优化:服务器端通常要处理大量并发连接,更倾向被动接受;客户端更灵活,主动发起、主动关闭。

6.在 Android/Java 层面的体现

  • 我们写 Socket client = new Socket(host, port),它的内部就是那一套“主动→SYN→SYN+ACK→ACK”流程;
  • 而写 ServerSocket server = new ServerSocket(port); Socket sock = server.accept();,是完全被动地等连接到来,accept 里并不会自己发任何包;
  • 如果我们在移动网络下做长连接,往往还要自己在应用层维护心跳,而不是让 TCP 对称地自动双向发心跳,因为那样负载会翻倍。

——
总结
TCP 不是对称的,恰恰是为了让它既能可靠地建立和拆除全双工连接,又能避免“死等”、“旧报文误入”和“双方同时关闭”的种种边界问题。

  1. 单工(Simplex)

    • 特点:信息只能沿一个方向流动。
    • 比如:电视广播、广播电台,它们只从“中心”发射,用户只能接收,不能回传。
  2. 半双工(Half‑Duplex)

    • 特点:双方都能发和收,但在同一时刻只能有一方在发送,另一方必须等待。
    • 比如:对讲机(Walkie‑Talkie):当一端在说话时,另一端只能听;说话完按下“PTT”键才切换到发射模式。
    • 理解要点:就像一条单车道的公路,车可以双向通行,但任何时刻只能一个方向的车通行。
  3. 全双工(Full‑Duplex)

    • 特点:双方可以同时发送和接收信息,互不阻塞。
    • 比如:普通电话通话,你说话的同时对方也能说,你们各自听到的声音是并行传输的。
    • 在网络上,TCP 连接就是典型的全双工通道:客户端和服务器可以同时读写流,互不干扰。

HTTP请求报文和响应报文的格式

一、HTTP 请求报文的格式

  1. 请求行(Request Line)

    • 结构:方法(Method) + 空格 + 请求 URI + 空格 + 协议版本 + 回车换行
    • “方法”常见有 GET、POST、PUT、DELETE、HEAD、OPTIONS 等;“请求 URI”可以是绝对路径或完整 URL;“协议版本”常用 HTTP/1.1。
    • 我在调试接口时,最常见的就是:
      GET /api/user/info HTTP/1.1⏎
    • 这是报文的“第一印象”,服务器就是从这行开始解析你的意图。
  2. 请求头部(Headers)

    • 紧跟在请求行之后,每一行“字段名: 值”格式,以回车换行结束。
    • 常用字段:
      • Host: 指定要访问的主机和端口(HTTP/1.1 必须)
      • User‑Agent: 客户端类型
      • Accept / Accept‑Encoding / Accept‑Language: 告诉服务器客户端能接受的内容类型、编码和语言
      • Content‑Type: 请求体类型(POST、PUT 有请求体时)
      • Content‑Length: 请求体长度,或 Transfer‑Encoding: chunked
      • Connection: keep‑alive 或 close,用于控制长连接
    • 例如,我在 Android 上写网络请求时,经常手动添加 Authorization、Cookie 等自定义头。
  3. 空行(CRLF)

    • 在所有请求头行之后会有一个空行(仅“回车换行”),作为头部结束的标志。
    • 服务器读到这个空行后,就知道后面要么开始读请求体,要么到此为止(GET、HEAD 请求通常无体)。
  4. 请求体(Message Body,可选)

    • 只有在需要提交数据时(如 POST 提交表单或 JSON、文件上传),才会有请求体。
    • 长度由 Content‑Length 或者分块传输(Transfer‑Encoding: chunked)来标识。
    • 我在接口联调时常碰到编码错误:JSON 里多了 BOM 或者长度计算不对,服务器就会报 400。

二、HTTP 响应报文的格式

  1. 状态行(Status Line)

    • 结构:协议版本 + 空格 + 状态码 + 空格 + 原因短语 + 回车换行
    • 常见状态码:200 OK、301 Moved Permanently、302 Found、400 Bad Request、401 Unauthorized、404 Not Found、500 Internal Server Error 等。
    • 比如服务器正常返回数据时,我会看到:
      HTTP/1.1 200 OK⏎
    • 这是客户端判断请求是否成功的第一道关卡。
  2. 响应头部(Headers)

    • 紧接状态行之后,每行“字段名: 值”,以回车换行结束。
    • 常用字段:
      • Date / Server: 时间戳和服务器标识
      • Content‑Type: 返回内容类型(text/html、application/json、image/png 等)
      • Content‑Length: 响应体长度,或 Transfer‑Encoding: chunked
      • Connection: keep‑alive / close
      • Cache‑Control / Expires / ETag: 缓存策略
      • Set‑Cookie: 服务器下发 Cookie
    • 在调试缓存与重试逻辑时,我会重点关注这些头的值是否合理,以及它们对客户端行为的影响。
  3. 空行(CRLF)

    • 与请求一样,用一个空行结束头部,告诉客户端下面开始是真正的数据体。
  4. 响应体(Message Body,可选)

    • 包含 HTML、JSON、图片、视频流等实际内容。
    • 体的读取同样依赖 Content‑Length 或 chunked 标记。
    • 我在处理分块编码时,经常在客户端用定长 buffer 循环读取,注意最后一个 “0⏎⏎” 才算真正结束。

三、几个关键细节和面试官常考点

  1. 首行和头部之间的 CRLF (回车换行)

    • 一定要有两次连续的 CRLF,第一次分割首行与头部,第二次分割头部与消息体。漏写或写成 LF 会导致服务器/客户端卡住。
  2. 字符编码

    • 报文头是 ASCII 编码;消息体常用 UTF‑8、ISO‑8859‑1 等。头部如 Content‑Type: application/json; charset=utf‑8 要写对。
  3. 持久连接与管道化

    • HTTP/1.1 默认 keep‑alive;Connection: close 可以关闭。
    • 管道化(pipelining)可以在一个连接里并行发多个请求,但实际浏览器支持有限、容易出现队首阻塞。
  4. 分块传输编码(Chunked)

    • Transfer‑Encoding: chunked 后,响应体被拆成若干块,每块前面写块长度的十六进制数。
    • 这允许服务器边生成边发送,对大文件下载或实时推流很有用。
  5. 报文大小和安全

    • 头部字段数量和大小不宜过大,否则可能触发 Web 服务器或代理的限制,引发 431 Request Header Fields Too Large。
    • 请求体大小要和接口设计对齐,避免出现 Content‑Length 与实际长度不符的安全漏洞。

总结

  • 无论请求还是响应,都严格按照 “首行 → 头部(多行)→ 空行 → 可选消息体” 这个四段式来组织。
  • 面试时我会强调:
    1. 首行(请求行/状态行)的结构和含义;
    2. 头部常见字段和它们在通信流程中的作用;
    3. 空行的必要性;
    4. 消息体的长度标记与传输方式。

讲讲加密协议,数字证书加密过程 (HTTPS)

  1. 为什么要用 HTTPS 加密

    • 数据机密性:防止中间人(MITM)窃听,确保客户端和服务器之间传输的内容如登录凭证、支付信息、用户隐私不被泄露。
    • 数据完整性:防止数据被篡改,客户端收到的数据就是服务器发出的数据,没有中途被恶意修改。
    • 身份验证:确保客户端连接到的是真正的服务器,而不是冒充者,从而避免钓鱼、伪造登录页面等安全风险。
  2. HTTPS 用到的两大核心技术
    一是 对称加密,二是 非对称加密(公钥加密/数字签名),再加上 哈希校验

    • 对称加密(如 AES):加密、解密用同一把密钥,速度快、适合大数据量加密。
    • 非对称加密(如 RSA、ECC):用一对密钥(公钥+私钥),公钥加密只能私钥解密,或者私钥签名公钥验证。速度比对称算法慢,适合少量数据(如密钥交换、数字签名)。
    • 哈希算法(如 SHA-256):把任意长度数据变成固定长度的“摘要”,用来校验数据完整性。
  3. 数字证书和证书链

    • 数字证书就是 CA(Certificate Authority,证书颁发机构)用自己私钥给服务器公钥做的“签名”,里面包含服务器的域名、服务器公钥、有效期、CA 名称等。
    • 当我们访问 https://api.example.com 时,服务器会把自己的证书(及中间 CA 证书链)一起发给客户端。
    • 客户端(Android 系统或 OkHttp)内置了一组根 CA 公钥,用来验证这条链路上每一级证书的签名。如果任一级签名校验失败,或者域名与证书不一致,就拒绝连接。
  4. TLS 握手过程(以 TLS1.2 为例)
    在真正开始加密通信前,客户端和服务器要先“握个手”,协商出一把对称密钥,并完成双向认证(客户端验证服务器、可选的服务器验证客户端):

    1. ClientHello
      • 客户端发起第一个消息,告诉服务器自己支持哪些协议版本(比如 TLS1.2)、哪些加密套件(如 ECDHE_RSA_WITH_AES_128_GCM_SHA256)和一个随机数 ClientRandom
    2. ServerHello + 证书
      • 服务器响应一个随机数 ServerRandom,并从它支持的套件里选一个与客户端匹配的加密套件。
      • 随后发送自己的数字证书链:服务器证书、中间 CA 证书等。客户端用根 CA 验证链路,并检查域名和证书有效期。
      • (可选)服务器还可以要求客户端证书,用于双向 TLS,但在大多数 HTTP 场景下只做单向验证。
    3. 密钥交换
      • 基于选定的套件,如果是 ECDHE(椭圆曲线 Diffie-Hellman)模式,服务器会发送它的 DH 公钥参数。
      • 客户端用服务器公钥和 ClientRandomServerRandom 计算出一个“预主密钥”(Pre-Master Secret),再用服务器公钥加密后发回去。
      • 服务器用自己私钥解密拿到同样的 Pre-Master Secret。
    4. 生成对称会话密钥
      • 双方用 ClientRandomServerRandomPre-Master Secret 通过相同的算法生成对称密钥(Session Key),包括用于加密和用于消息完整性校验的密钥。
    5. Finished 消息
      • 客户端和服务器各自发送一条用会话密钥加密的“Finished”消息,用来验证握手过程中的所有数据都没被篡改。只有正确生成了对称密钥的一方才能解密并校验成功。
    6. 进入加密通信
      • 握手结束后,后续的应用层数据(HTTP 请求和响应内容)就用这把对称密钥加密,保证机密性和完整性。
  5. Android 上的实践和注意点

    • 系统 TrustStore:Android 系统或 Google Play 服务里预装了一些根 CA,OkHttp、HttpURLConnection 都会用它来验证服务器证书。
    • 证书 Pinning:为防止中间人用恶意 CA 颁发假证书,有时我们会把自己的服务器公钥或证书指纹写在 App 里,只有匹配才允许连接。这样能更安全,但也要考虑证书更新带来的 App 版本兼容问题。
    • TLS 版本和套件:最新 Android 版本默认支持 TLS1.2/1.3,较老版本可能只到 TLS1.0。务必在 App 初始化时检查并开启高版本协议;同时尽量避免 RC4、3DES、SHA1 等已知不安全的加密算法。
    • 性能优化:HTTPS 握手开销较大,Android 开发中常用 HTTP Keep‑Alive 复用长连接,或者用 HTTP/2 在同一连接上并发多路复用请求,减少握手次数。

——
总结一下,我一般会这样回答:

“HTTPS 是在 HTTP 之上加一层 TLS/SSL:先用非对称加密和数字证书做身份验证和密钥协商,生成一把对称会话密钥,然后用对称加密和 HMAC 保护后续的数据传输。数字证书则由受信任的 CA 签发、客户端验证整个证书链,确保服务器身份。Android 上我们还要考虑系统 TrustStore、证书 Pinning、网络库对 TLS 版本和加密套件的支持,以及长连接和 HTTP/2 来减小握手开销,这样才能在保证安全的前提下,兼顾性能和兼容性。”

常用的状态码

1xx(信息响应)

  • 这一类状态码在移动端非常罕见,Android 网络库也几乎不会暴露给我们,一般不用特别处理。

2xx(成功)

  • 200 OK:最常见的成功响应,表示服务器已经成功处理了请求。
    场景:请求数据列表、详情页,拿到业务正常数据后直接解析并展示。
  • 201 Created:资源创建成功时返回,常见于 POST 新增场景。
    场景:我在提交“新建订单”或“发布评论”后,后端返回 201,同时在响应头里带上 Location 或者在响应体里返回新资源的 ID。客户端可以根据这个 ID 立即刷新界面。
  • 204 No Content:服务器成功处理了请求,但没有返回任何内容。
    场景:删除操作(DELETE)后,服务器通常返回 204。Android 端收到后只要判断状态码,就认为删除成功,然后从界面列表里移除相应 item。

3xx(重定向)

  • 301 Moved Permanently/302 Found:表示资源已被永久或临时重定向到新的 URL。
    场景:在浏览器里比较常见,移动端请求 REST API 很少用到,除非后端做版本升级把接口地址换了。一般我们会在网络库层面自动跟随跳转,也会关注最终是否拿到 200。
  • 304 Not Modified:客户端通过 If‑Modified‑Since 或 ETag 发起条件请求,服务器判断缓存依然有效就返回 304,不返回新内容。
    场景:我在实现图片或列表数据的缓存时,会优先设置 If‑None‑Match/If‑Modified‑Since,拿到 304 就直接走缓存,大大节省流量和加载时间。

4xx(客户端错误)

  • 400 Bad Request:请求参数格式错误,服务器无法解析。
    场景:我把 JSON 或表单格式写错,后端就会给 400。开发阶段我会在 Logcat 里打印完整请求体和 URL,快速定位是哪一个字段没传或类型匹配失败。
  • 401 Unauthorized:身份验证失败或 token 过期。
    场景:我用 OkHttp 拦截到 401,就触发自动刷新 token 流程(或者跳到登录页)。这是移动端最常见的错误之一。
  • 403 Forbidden:权限不足,比如用户没有访问某个资源的权限。
    场景:有些接口只有 VIP 用户或者管理员才能调用,普通用户调用后会被拒绝,这时客户端会显示“权限不足”提示框。
  • 404 Not Found:请求的资源不存在。
    场景:调用了错误的 API 路径或者资源 ID 已被删除,客户端通常会弹出“内容已不存在”或走降级逻辑。
  • 408 Request Timeout:客户端请求超时。
    场景:网络不稳定时偶尔遇到,Android 端要捕获并给用户“网络不佳,请重试”的提示,同时可做重试策略。
  • 429 Too Many Requests:客户端在短时间内发送了过多请求,被服务器限流。
    场景:列表刷新的按钮点太快,或者轮询频率太高,会触发限流。客户端可以在收到 429 时做退避重试(exponential backoff)。

5xx(服务器错误)

  • 500 Internal Server Error:服务器内部错误,最通用的 5xx 返回。
    场景:后端日志未预料到的异常,Android 端只能给出“服务器异常,请稍后再试”的用户友好提示,建议上报埋点或反馈给产品。
  • 502 Bad Gateway/503 Service Unavailable/504 Gateway Timeout:通常是后端网关或负载均衡层面的故障。
    场景:短期内全局都访问失败时,多数是这些状态码。客户端可以触发一次全局网络状态检查,或者限流报警,但不宜自动多次重试,以免雪崩。

讲讲进程调度算法

  1. 先来个分类总览
    · 非抢占式 vs 抢占式
    – 非抢占式算法一旦给了 CPU,进程就会一直运行到自己释放(或阻塞、终止)。
    – 抢占式算法可以随时中断正在运行的进程,把 CPU 分给别的进程。
    · 批处理调度 vs 交互式调度
    – 批处理环境更关注吞吐量和作业完成时间(Turnaround Time)。
    – 交互式环境更关注响应时间和公平性。

  2. 几种经典算法及其特点

    1. 先来先服务(FCFS / FIFO)
      • 原理:按进程到达就绪队列的先后顺序调度。
      • 优点:实现简单、开销小。
      • 缺点:容易产生“短进程饥饿”——如果前面来了一个耗时很长的进程,后面所有短进程都要排队等待。平均等待时间也不一定最优。
    2. 最短作业优先(SJF)/最短剩余时间优先(SRTF)
      • 原理:挑选预计运行时间最短的进程先跑;SRTF 是抢占式版本,如果有更短的新进程到来,就把当前进程抢下来。
      • 优点:能最小化平均等待时间。
      • 缺点:必须知道或估计进程执行时间;可能导致长作业“饥饿”。
    3. 时间片轮转(Round Robin)
      • 原理:给每个进程一个固定长度的时间片(quantum),时间片用完就放到队尾、切换到下一个就绪进程。
      • 优点:能保证每个进程都有机会跑,响应性好,适合交互式系统。
      • 缺点:时间片选得太小会导致频繁上下文切换,开销高;太大又退化为 FCFS。
    4. 优先级调度(Priority Scheduling)
      • 原理:为每个进程分配优先级,CPU 优先给高优先级的进程。可以是静态优先级,也可以动态调整。
      • 优点:能体现不同任务的重要性。
      • 缺点:低优先级进程可能“饥饿”;需要饥饿防护(aging),即随着等待时间增加提高其优先级。
    5. 多级队列/多级反馈队列(Multilevel Queue / Multilevel Feedback Queue)
      • 原理:将就绪进程分到多个队列,不同队列有不同的调度策略(如前台队列用 RR,后台队列用 FCFS)。多级反馈队列还允许长时间占用 CPU 的进程逐渐往低优先级队列“下沉”,保持系统公平性。
      • 优点:既能兼顾响应性,也能兼顾吞吐量;能动态区分短作业和长作业。
      • 缺点:实现较复杂,需要调优队列数量、时间片大小、优先级调整策略。
  3. Linux/Android 上的实际调度:CFS(完全公平调度器)

    • Linux 从 2.6.23 开始默认采用 CFS(Completely Fair Scheduler)。它不再用固定时间片,而是以“虚拟运行时间”(vruntime)为度量,尽量让每个可运行进程在理想情况下获得相同的 CPU 份额。
    • 核心思想:把所有可运行进程按 vruntime 排序,最小 vruntime 的那个先跑。谁跑了,就把它的 vruntime 按照实际运行时间乘以其权重(nice 值映射的权重)加上去,放回红黑树。
    • 优点:
      • 极大提高公平性:按照进程的权重(nice 值)自动分配 CPU 时间,抢占式且不会偏袒某一个进程。
      • 响应性好:短进程、I/O 交互进程(nice 值默认)能更快回到 CPU 上。
    • 在 Android 设备上,内核同样通过 CFS 来调度各个进程和线程,系统服务、Binder 调用、应用主线程、渲染线程之间都遵循这个机制。

recyclerView的缓存机制

  1. 三层缓存结构
    RecyclerView 内部对 ViewHolder 有三级缓存:
    a. Attached Scrap(附着废弃池)
    • 存放刚从屏幕上滚出但还“热乎”的 ViewHolder,尚未完全脱离 RecyclerView 管理。
    • 通常只有在布局(Layout)阶段才会把当前屏幕上所有没用到的子 View 暂存在这里,以便快速重新绑定。
    b. Cached Views(视图缓存池)
    • 当一次 Layout 完成后,多余的 ViewHolder 会从 Attached Scrap 转入这里,默认能缓存 viewCacheSize=2 个同类型的 View。
    • 这一级缓存是针对单个 RecyclerView 实例的,重绑定速度比完全重绘要快很多。
    c. RecycledViewPool(全局回收池)
    • Cached Views 数量超过上限后,多余的 ViewHolder 就会被推到这个全局池,既可以被当前 RecyclerView 再利用,也能被别的 RecyclerView 复用。
    • 可以通过 recyclerView.setRecycledViewPool() 共享,同一个类型的 View 最多会调用 pool.setMaxRecycledViews(type, count) 来控制最大数量。

  2. 缓存消费流程

    1. 用户滑动,产生新的位置需要展示,RecyclerView 先尝试从 Attached Scrap 找到同一 viewType 的 ViewHolder;
    2. 找不到再去 Cached Views 池拿;
    3. Cached Views 拿不到再去 RecycledViewPool;
    4. 如果全局池也空,就真正走 onCreateViewHolder() 创建新的。
    5. 拿到后,把它标记为“正在使用”,调用 onBindViewHolder() 更新数据并布局。
  3. 为什么要三层?

    • 低延迟反馈:刚滚出屏幕的 ViewHolder(Attached Scrap)往往很快又会回到屏幕,优先复用能最大程度避免抖动。
    • 合理容量控制:Cached Views 数量不宜过多,因为会占用内存;默认缓存 2 个,就能兼顾上下滑时常用的两屏范围。
    • 跨 Recycler 共享:同类型列表如果在同一个界面多个,或者不同界面但都用同一种 item 布局,复用成本最低。
  4. LayoutManager 与预取

    • LinearLayoutManager、GridLayoutManager 等在测量与布局阶段,会先计算出一批可能马上要显示/预加载的 View,直接从缓存取或 inflate。
    • 你还可以调用 setItemPrefetchEnabled(true) 或者 setInitialPrefetchItemCount(n)(在 RecyclerViewPool 共享时)让 child RecyclerView 也提前填充,这在嵌套场景里尤其明显。
  5. 我在项目中遇到的优化思路

    • 调大 viewCacheSize:在复杂 item、onCreateViewHolder 成本极高、列表滚动卡顿时,我把缓存数从默认 2 调到 5~10,但也要注意内存攀升。
    • 共享 RecycledViewPool:对同一个 Activity 里多个相似列表,我统一创建一个 RecycledViewPool,避免重复 inflate,滑动流畅度提升约 10%~20%。
    • 分 viewType 管理:对几种截然不同的 item 布局要分别设置不同的最大回收数,避免某一种类型抢光了全局池导致另一种类型频繁重建。
    • 避免外部干预缓存:不要在 Adapter 的 onDetachedFromRecyclerView 或者父布局里手动调用 clear(),否则完全失去复用效果。
  6. 常见坑与注意

    • Item 动态增删:如果你在滑动时同时增删数据,RecyclerView 可能先把某个 ViewHolder 放进 scrap,再立刻被移除,最终导致状态不一致或闪烁。要用好 notifyItemInserted/Removed,而不是 notifyDataSetChanged()
    • 状态复用问题:ViewHolder 复用时,如果有 CheckBox、Switch、选中状态等 UI 控件,一定要在 onBindViewHolder 里手动还原,不能指望 RecyclerView 自动清零。
    • NestedScrolling:在嵌套 RecyclerView 里,prefetch 与 cache 冲突会导致无用 View 不断进出池,我一般在外层关闭预取、让内层自己管理更稳定。

listView和recyclerView的区别 ListView 是 Android 平台上较早期用来显示大量可滚动列表项的控件

  1. 架构层次

    • ListView 是 AdapterView 的子类,内部把“布局、滚动、复用”都耦合在一起;
    • RecyclerView 则拆成了四大模块:视图容器(RecyclerView)+ 布局策略(LayoutManager)+ 复用池(Recycler)+ 动画/装饰(ItemAnimator、ItemDecoration),每块都可独立替换或扩展。
  2. 强制 ViewHolder 模式

    • ListView 虽有 convertView 复用,但 ViewHolder 只是一种推荐写法,不强制;
    • RecyclerView 在 API 设计里把 ViewHolder 变成必需:onCreateViewHolder/onBindViewHolder 分离,彻底杜绝反复 findViewById。
  3. 缓存与复用能力

    • ListView 只有一个 convertView 池,滚出屏幕后复用率有限;
    • RecyclerView 有三级缓存(Attached Scrap、Cached Views、RecycledViewPool),不仅列表内高效复用,还能跨 RecyclerView 共享同类型 ViewHolder。
  4. 布局和滚动策略

    • ListView 只能做垂直单列;想做网格、瀑布就得换 GridView 或第三方;
    • RecyclerView 由 LayoutManager 插件化支持线性、网格、瀑布、甚至自定义任意布局。
  5. 动画和装饰

    • ListView 没有内建的增删改动画,分割线只能靠自定义 Drawable 或在 Adapter 里手动插入占位布局;
    • RecyclerView 自带 ItemAnimator,可对 notifyItemInserted/Removed/Changed 自动执行动画;ItemDecoration 让分割线、边距、背景更轻松可控。
  6. 使用成本 vs 可扩展性

    • ListView API 简单,上手快、代码少,适合需求极其简单的场景;
    • RecyclerView 则要手动配置 LayoutManager、Decoration、Animator,模板代码多一点,但日常遇到复杂列表(多类型、拖拽、侧滑、预取)时,能省下无数“改造痛点”。

总结:如果项目里只有一个简单的垂直列表、没动画也没性能瓶颈,用 ListView 快;但面对复杂场景和高流畅度要求,RecyclerView 的解耦复用和插件化能力更贴合现代 Android 开发。

java注解相关

  1. 注解是什么
  • 注解本质上是附加在类、方法、字段、参数甚至包上的“元数据”。
  • 它不改变程序逻辑,而是给编译器、工具或运行时提供额外的信息。
  • 在源码层面你会看到形如 @Override@Nullable@Inject 这样的标记。
  1. 注解的分类和元注解
  • 按用途分:
    • 标记型(Marker):没有元素,比如 @Override
    • 单值型:只有一个属性,如 JUnit 的 @Test(timeout=1000)(只有一个 timeout)。
    • 完整型:多个属性,每个都有 key‑value。
  • 按保留范围(由元注解 @Retention 决定):
    • SOURCE:仅存在源码,编译后丢弃(IDE 校验、Lombok、ButterKnife 注解生成时用)。
    • CLASS:编译后保留在 .class 文件,但运行时不可见(字节码工具、AspectJ、一些静态分析用)。
    • RUNTIME:编译后加载到 JVM,可通过反射读取(依赖注入框架、ORM、序列化库最常用)。
  • 按作用目标(由元注解 @Target 决定):
    • TYPE(类、接口、枚举)
    • METHOD(方法)
    • FIELD(成员变量)
    • PARAMETER(方法参数)
    …等等,确保注解只能用在合适的位置。
  1. 注解的常见场景
  • 编译期校验和代码生成(APT,Annotation Processing Tool)
    • Dagger、ButterKnife、Room、AutoValue 等,用注解标记接口或模型,再生成辅助类。
    • 优点:编译时就定位错误,运行时没有性能损耗。
  • 运行时动态行为(反射解析)
    • Gson、Jackson 序列化时读取 @SerializedName
    • Retrofit 用注解描述 HTTP 请求方法和参数(如 @GET@Path@Body);
    • 自定义 DI 框架在运行时扫描带 @Inject 的构造器或字段并注入实例。
  • 工具和 IDE 支持
    • @Override@Nullable/@NonNull、AndroidX 的 @UiThread@WorkerThread,帮助 IDE 在编译期给出警告或快速跳转文档。
  1. 自定义注解与处理流程
  • 定义:
    • 写一个 @interface MyAnnotation { ... },并配上 @Retention@Target
    • 按需加上 @Documented@Inherited 等元注解。
  • 处理:
    • 编译期:实现一个 javax.annotation.processing.Processor,在 process() 里扫描注解元素并生成代码或报告错误。Gradle 的 APT 插件会自动把它挂到编译链上。
    • 运行时:用反射(Class.getAnnotations())或第三方库(Reflections)遍历类路径,做动态绑定或行为切面。
  1. 我在项目里的实践要点
  • 优先用编译期注解处理来生成代码(性能无损且能提早发现问题)。
  • 对于需要动态配置或插件化场景,用运行时注解配合反射,但要注意减少扫描范围以免影响启动速度。
  • 自定义注解时务必明确 @Retention 和 @Target,避免误用或编译后拿不到数据。

注解如何绑定

  1. 定义注解
    • 首先要写一个注解类型,标记上它的作用目标(@Target,比如 FIELD、METHOD、TYPE)和保留策略(@Retention,SOURCE 或 RUNTIME)。
    • 比如要给 Activity 里控件字段绑定 findViewById,就定义一个 @BindView(int value) 的注解,value 存 id。

  2. 编译期绑定(APT 方式)
    核心思路:在编译时由工具读取注解、生成对应的“Binder”类,运行时直接调用生成的代码完成绑定,性能无反射开销。
    步骤:

  1. 写一个注解处理器(extends AbstractProcessor),在 process() 里:
    – 扫描项目源码中所有带 @BindView 的元素(Element);
    – 按宿主类(Activity/Fragment)分组,同一个类下的注解聚合到一份列表;
    – 用 Filer API 生成一个 Xxx_ViewBinding.java,里面写好构造函数或 bind() 方法,通过 findViewById 把 view 赋值给字段。
  2. 编译器在编译时执行这个处理器,输出绑定类。
  3. 在 Activity 里只要调用 ButterKnife.bind(this)(其实它就是 new Xxx_ViewBinding(this)),所有 @BindView 字段就被自动赋值了。

优点:运行时性能好,编译就能发现注解用法错误;缺点是要写或依赖一个 APT 库,生成代码结构相对复杂。

  1. 运行时绑定(Reflection 方式)
    核心思路:在 App 启动或使用时,用反射遍历类和字段,遇到注解就读取它的属性值,动态执行绑定逻辑。
    步骤:
  1. 在一个统一入口(比如自定义框架的初始化类、Activity 的 onCreate)里,通过 Class.forName 或者传入一个对象实例:
    – 拿到它的 Class,然后调用 getDeclaredFields()/getDeclaredMethods();
    – 对每个 Field/Method 调用 isAnnotationPresent(BindView.class);
    – 如果有,就拿到注解实例 bindView = field.getAnnotation(BindView.class),
    读取 bindView.value(),执行 field.setAccessible(true);
    再用 host.findViewById(id) 的结果去给 field.set(host, view)。
  2. 对方法甚至接口路由也类似:扫描带 @Route 注解的方法,缓存一个 Map<path, Method>,运行时按 path 找到对应 Method.invoke(...)去跳转。

优点:使用简单,注解处理器不用单独维护;缺点:反射慢、启动/首次调用时要花时间扫描。

  1. 面试官想听的关键要点
    • 两种方式的区别——编译期(APT) vs 运行时(Reflection),以及它们的优缺点;
    • 从“读到注解”到“关联到业务逻辑”的流程:扫描(AbstractProcessor 或 Class反射)→ 解析注解属性 → 生成或执行绑定代码;
    • 在 Android 中真正用到的场景:View/Listener 绑定(ButterKnife)、依赖注入(Dagger)、路由机制(ARouter)、ORM/序列化映射(Room、Gson)等。

java的锁了解哪些

  1. 内置锁(synchronized)

    • 本质是对象监视器(Monitor),JVM 通过偏向锁→轻量级锁→重量级锁动态演进,减少无竞争或轻度竞争场景下的性能开销。
    • 特点:
      • 语法层面直接支持(修饰方法或代码块),写法简单。
      • 是可重入锁,线程获得锁后可以多次进入同一个监视器。
      • 隐含一个等待/通知队列(wait/notify)。
    • 场景:
      • 保护少量临界区、简单同步;
      • 不需要精细控制唤醒顺序/超时/中断的场合。
  2. 显式锁(java.util.concurrent.locks.Lock)
    a. ReentrantLock

    • 特点:
      • 可重入,与 synchronized 同样保证同一线程可重入。
      • 支持公平锁/非公平锁:公平锁按请求顺序排队;非公平锁吞吐量高。
      • 提供 tryLock(可立即失败或带超时)、lockInterruptibly(可被中断)等更灵活的获取策略。
      • 通过 Condition 对象创建多组等待队列,替代单一的 wait/notify。
    • 场景:
      • 想要控制超时抢锁、响应中断或自定义唤醒顺序时;
      • 需要多个条件队列(如生产者/消费者多个条件)时。

    b. ReentrantReadWriteLock

    • 特点:
      • 拆分读锁/写锁:多个读线程可并发获得读锁,写锁独占。
      • 支持公平/非公平模式。
    • 场景:
      • 读多写少的数据结构(缓存、配置共享),提升并发度;
      • 对读写隔离有严格要求、希望尽量避免写时阻塞所有读时。

    c. StampedLock(Java 8)

    • 特点:
      • 在读写锁之上引入“戳记”(stamp)概念,支持三种模式:写锁、悲观读锁、乐观读锁。
      • 乐观读模式无需真正阻塞,读时只记录一个版本戳,最后校验戳是否改变即可。
      • 成本比 ReentrantReadWriteLock 低,吞吐量更高,但不支持可重入。
    • 场景:
      • 高并发读场景,非常少量写;
      • 对可重入要求不高,愿意在失败后降级为悲观锁的场合。
  3. 底层优化与无锁思路

    • 偏向锁和轻量级锁:JVM 在无竞争到低竞争场景用 CAS 合并锁标志,减少操作系统互斥开销。
    • LockFree/CAS:在极端高并发且对延迟敏感的场景,使用 AtomicInteger、AtomicReference 等原子类或 LongAdder,避免阻塞。
  4. 在 Android 项目中的实践要点

    • 对 UI 线程绝不加锁,避免 ANR;在高并发计算或共享资源保护里才用 synchronized 或 ReentrantLock。
    • 如果数据读多写少,优先考虑 ReadWriteLock 或者用 CopyOnWriteArrayList、ConcurrentHashMap 这类并发容器。
    • 遇到复杂的生产者—消费者,倾向用 ReentrantLock+Condition 组合,写出可超时、可中断、可唤醒指定线程组的队列。
    • 对低争用的简单锁用 synchronized;对复杂需求(可中断、超时、分组唤醒、读写分离)用显式锁。

Concurrenthashmap的原理 (读操作是不用锁的,写需要)

  1. 为什么要用 ConcurrentHashMap

    • 普通的 HashMap 在并发场景下会产生数据不一致甚至死循环;
    • Collections.synchronizedMap(new HashMap<>()) 虽然线程安全,但所有读写都要竞争同一个锁,吞吐量低;
    • 而 ConcurrentHashMap 提供了高并发下的读写性能和可见性保证,是更适合多线程场景的 Map 实现。
  2. Java 7 与 Java 8 的设计演进

    • Java 7:基于分段锁(Segment Locking)
      • 将整个桶数组(Table)划分成若干个 Segment(默认 16 段),每个 Segment 管一个子数组;
      • 每次 put/remove/compute 只锁定对应 Segment 的锁,不同 Key(落在不同段)可以并行写;
      • get 操作直接用 volatile 读,无需加锁,读—写几乎无阻塞。
    • Java 8:去掉了 Segment,改为节点级别的 CAS + synchronized
      • 依旧是一个 volatile 的 Node<K,V>[] table 桶数组,但每个桶头(一个链表或红黑树)都是单独加锁的:
        1. get:全程无锁,只用 volatile 读取链表/树节点,遍历即可。
        2. put:先 CAS 尝试把新节点插入空桶头;若失败或桶已存在,则进入 synchronized(block on that bucket) 做链表尾插或树化。
        3. 树化(TreeBin):当同一个桶里节点过多(默认 >8 且 table 大小足够)时,从链表转换成红黑树,减少遍历开销。
      • 扩容(resize):不再一把全局锁,而是用 CAS 标记「正在扩容」的转发节点(ForwardingNode),并让多个线程分段协助完成旧表到新表的搬移,减少停顿。
  3. 并发控制与性能特点

    • 读不加锁:绝大多数场景下 get 操作都是纯 volatile 读,几乎无争用开销。
    • 写时粒度小锁:只在桶级(链表头或树根)加锁,不会阻塞其他桶的读写。
    • 无死锁风险:锁的粒度固定、链路简单,没有跨桶锁定或多级锁请求,避免死锁。
    • 高并发下线性扩容协助:扩容时写线程会「帮忙搬迁」一部分节点,而不是停掉所有业务线程,缩短扩容暂停时间。

Hashtable内部原理

  1. 基本定位和历史背景

    • Hashtable 是 JDK 1.0 时代就有的 Map 实现,主要特点是“线程安全”,它把所有主要操作(getputremove)都加了 synchronized
    • 在 Java 2(即 1.2)里,HashMapCollections.synchronizedMap、以及后来的并发集合(ConcurrentHashMap)出现后,Hashtable 就被认为是“过时的”但还保留着,主要为了兼容老代码。
  2. 线程同步机制

    • 内部所有对桶数组(Entry 数组)的访问都在方法级别加锁,也就是说同一个时刻只能有一个线程在读或写。
    • 优点是实现简单,使用它天然不用再在外层做同步;缺点是锁粒度太粗,读操作也要等待锁释放,性能在高并发场景下瓶颈非常明显。
  3. 性能与迭代特性

    • 因为所有方法互斥,Hashtable 在多线程读大量数据时反而比单线程的 HashMap 慢。
    • 它的迭代器是“安全的快照式”还是“fail‑fast”?实际上,Hashtable 的 EnumerationIterator 都是实时遍历,如果在遍历期间有其他线程结构性修改,就会抛出 ConcurrentModificationException(它内部用一个修改计数检测)。
    • 由于锁住整个对象,迭代过程中依旧会锁表,基本不会出现弱一致性,但同样影响吞吐。
  4. 与 HashMap、ConcurrentHashMap 的对比

    • HashMap:非线程安全,但无锁开销,允许 null 键和值,扩容机制更灵活(按 2 倍扩容);在单线程或有外部同步时更高效。
    • Collections.synchronizedMap(new HashMap<>):方法同步和 Hashtable 类似,但还是把锁在 map 对象上;在迭代时需要手动在外层用 synchronized(map) 来包裹,防止 ConcurrentModificationException
    • ConcurrentHashMap:细粒度或无锁读写(Java 7 用分段锁,Java 8 用桶级 CAS+锁、无锁读),性能高、弱一致性(读取可能看到更新前的数据),允许更高并发;不允许 null 键或值。
  5. 使用场景和实践建议

    • 如果你在接手一个老项目里的工具类库里看到 Hashtable,一般先评估能否安全迁移到 HashMap 或者 ConcurrentHashMap,既能去掉不必要的全表锁,也能享受现代实现的性能优势。
    • 只有在极少数业务非常简单、低并发、又不方便修改大规模代码时,才会继续保留 Hashtable
    • 在新的代码里基本不再主动选用 Hashtable,而是根据需要选 HashMap(单线程或自行同步)或 ConcurrentHashMap(高并发场景)。

CAS底层如何实现

  1. CAS 的本质:
    • CAS(Compare‑And‑Swap)是一种乐观并发策略,三个操作数——内存地址 V、旧值 A、新值 B——原子地比较并交换:
    – 如果内存地址 V 当前值等于期望的旧值 A,就把它更新为 B,返回 true;
    – 否则不改动,返回 false。
    • 依赖不断重试(自旋)直到 CAS 成功或放弃。

  2. 硬件层面的支持:
    • 现代 CPU(x86、ARM、PowerPC)都提供原子指令:
    – x86 上通过 CMPXCHG/CMPXCHG8B 等指令完成“比较并交换”;
    – ARM 上用 LL/SC(Load‑Link/Store‑Conditional)或 newer LDREX/STREX 指令对内存进行原子操作。
    • 这些指令内置了内存屏障(或在配合屏障指令使用),保证在多核架构上操作的可见性和顺序性。

  3. JVM 层面的映射:
    • Java 里 java.util.concurrent.atomic 包的原子类,通过 sun.misc.Unsafe(或 JDK 9+ 的 VarHandle)提供 compareAndSwapIntcompareAndSwapObject 等本地方法。
    • HotSpot 在 JIT 阶段,把这些本地方法标记为 intrinsic,直接生成对应平台的 CMPXCHG/LDREX/STREX 指令,并插入必要的读/写屏障(Acquire/Release Fence)。

  4. Android ART 上的实现:
    • ART 运行时对 Unsafe 或 JNI 原子操作同样映射到 bionic libc 或汇编 stub,调用 ARM 原子指令。
    • 你用 AtomicInteger.getAndIncrement(),底层会不断做:
    – 读 volatile 值(带 Acquire 语义);
    – 试 CAS 写新值(带 Release 语义);
    – 如果失败再读再试。

  5. 重试和 ABA 问题:
    • 由于是乐观锁,失败后会 spin 重试;
    • ABA(值先从 A 变到 B 又变回 A)会被误判为没改过,解决方案是引入版本号(如 AtomicStampedReference)或在业务上避免简单重用同一个值。

  6. 面试官关心的重点:

    • CAS 不是 Java 层模拟,而是真正依赖 CPU 原子指令;
    • JVM/ART 通过 Unsafe/VarHandle 做本地调用,并由 JIT 编译器内联成机器码;
    • 内存屏障确保多核可见性;
    • 乐观、无锁、需重试,也要注意 ABA。

有没有用过第三方库

  1. 网络和数据解析
    • Retrofit + OkHttp:
    – 我用它做网络请求几乎是标配,OkHttp 负责底层的连接池、缓存和拦截器,Retrofit 则通过注解把接口定义成方法。
    – 在实际项目里,我会统一配置超时、重试、日志拦截,并用 Retrofit 的 ConverterFactory(Gson 或 Moshi)来做 JSON 序列化/反序列化。
    • Gson / Moshi:
    – Gson 在项目里从老版到新版一直在用,不需要额外生成代码,兜底性能还可以;
    – 对于性能要求更高或者 Kotlin data class,我会用 Moshi+KotlinJsonAdapter,更好支持默认值与非空检查。

  2. 异步与响应式
    • RxJava:
    – 在早期项目中,我用 RxJava 管理异步流和链式网络/数据库混合请求,配合 Retrofit + RxAdapter,写起来非常流畅。
    – 但维护成本也比较高,订阅和取消的生命周期要管理好。
    • Kotlin Coroutines + Flow:
    – 在后期项目里,我逐步用 Coroutines 取代 RxJava,搭配 Retrofit Coroutine adapter 和 Room 的 suspend DAO 方法,代码更简洁,也更容易跟生命周期(LifecycleScope)绑定。
    – Flow 用来处理分页、实时数据流、UI 事件都很方便。

  3. 本地存储与 ORM
    • Room:
    – Jetpack 官方推荐,把 SQLite 映射成 DAO 接口,编译时生成 SQL 代码。
    – 我在项目里配置过多表联查、复杂事务、TypeConverter,调优索引,实际运行中性能和可维护性都很好。
    • MMKV 或 SharedPreferences+Jetpack DataStore:
    – 对于简单的 key-value 配置,我有时会用 Google 的 DataStore(基于 Proto 或 Preferences),替换原生 SharedPreferences,兼顾安全和异步写入。

  4. 图片/多媒体与 UI
    • Glide / Coil:
    – Glide 在高分辨率设备下缓存管理做得成熟;后来我在 Kotlin 项目里尝试过 Coil,它启动更快、集成 Coroutines。
    – 实际用时会配置占位图、圆角转化、优先级、内存/磁盘缓存策略。
    • Lottie:
    – 在交互动画场景里,用 Lottie JSON 动画可以做些复杂插画效果,不用自己写帧动画或 VectorDrawable。
    • Material Components 和 ConstraintLayout:
    – 虽然不是“第三方”但社区依赖度很高,我会在项目里统一用 Material 主题、Component、BottomSheet、Snackbar 以及 ConstraintLayout 做响应式布局。

  5. 依赖注入与工具链
    • Dagger-Hilt / Koin:
    – 我最早在项目里用过 Dagger2,显式写 Module/Component,后来接入 Hilt,大大简化了注入流程并自动生成代码。
    – 对于中小型项目,尝试过 Koin,它在写法上更“DSL 化”、启动快,但在复杂作用域管理上略逊;
    • LeakCanary、Timber:
    – LeakCanary 用来实时捕获内存泄露,开着几乎无运行时开销;
    – Timber 代替 Android 原生 Log,更灵活地在发布版屏蔽日志、按 tag 过滤,日常排查问题很方便。
    • 测试相关:
    – Mockito 或 MockK 用于单元测试的模拟;
    – Espresso + FragmentScenario 在 UI 自动化测试里保证 Activity/Fragment 持续稳定;
    – Retrofit 的 MockWebServer 做离线接口联调。

jetpack相关组件

  1. 架构与状态管理
    • Lifecycle 与 LifecycleObserver
    – 用途:让自定义组件(比如自定义 View、网络客户端或定时任务)能感知 Activity/Fragment 的 onStart/onStop/onDestroy,无须手动解绑。
    – 好处:统一管理资源,防止因为漏注册/注销带来的内存泄露或异常。
    • ViewModel + LiveData(或 StateFlow)
    – ViewModel:存放和管理 UI 相关数据,配置变更(旋转、后台回收)后数据依然可用;
    – LiveData:生命周期感知的数据容器,Activity/Fragment 只需观察,就能自动在对应生命周期内更新 UI,也不用担心手动 removeObserver。
    – 在我的项目里,ViewModel 里拿到网络/数据库结果后直接 postValue/emit,UI 层只做渲染,职责分离非常清晰。

  2. 本地存储与分页
    • Room
    – 用注解定义实体类和 DAO,编译期生成好增删改查的实现,还能校验 SQL 语句,避免运行时崩溃。
    – 我在项目里用过复杂的联合查询、事务操作,编译器都能帮我 catch 错误,而且它对 Coroutine/Flow、RxJava 都有原生支持。
    • Paging
    – 典型场景:列表要展示上千条或更大数据集时,Paging 能自动按页加载、回收可见之外的 item,还支持 RxJava/Coroutines 流式分页。
    – 我在做瀑布流、聊天列表时,引入 Paging 后不卡顿,也不用自己写加载更多、空视图和错误重试逻辑。

  3. 导航与异步任务
    • Navigation Component
    – 能用可视化的 NavGraph 定义页面、Action、DeepLink 和参数,SafeArgs 插件还自动给你生成类型安全的 Bundle 辅助类。
    – 我最早是手写 FragmentTransaction,很容易出错和漏回退栈;切到 Navigation 后,这些逻辑基本全交给框架,出错率大幅下降。
    • WorkManager
    – 用于做可持久化、可延迟、可链式的后台任务,比如日志上传、定时同步。即使进程被 kill、设备重启,它也能保证“最终一定执行”。
    – 我经常给它加上网络/充电/空闲等 Constraints,不用自己管 AlarmManager/JobScheduler 的兼容问题。

  4. 依赖注入
    • Hilt
    – 基于 Dagger,但对开发者做了极大简化:只要在 Application/Activity/Fragment 标上注解,框架就能自动构造 Component、注入 Retrofit、Room、WorkManager 等实例。
    – 我在项目里用 Hilt 后,模块之间的依赖关系一目了然,也方便做单元测试或替换实现。

讲讲retrofit

  1. Retrofit 的定位
  • 它本质上是一个类型安全的 HTTP 客户端:通过 Java(或 Kotlin)接口和注解,把 REST 或 HTTP 接口定义成方法签名。
  • 目的:把重复的 “构造请求–解析响应–错误处理” 这些样板代码,都交给框架去做,让开发者只管写接口和数据模型。
  1. 核心机制
    a. 接口+注解
    • 在接口方法上用 @GET@POST@PUT@DELETE 等注解标明 HTTP 方法和相对路径;
    • 用 @Path@Query@Field@Body 等注解绑定方法参数到 URL、表单、请求体;
    • 框架在运行时扫描这些注解,动态生成一个实现类,真正调用 OkHttp 发请求。

b. 底层网络:OkHttp
• Retrofit 并不自己实现 HTTP,而是用 OkHttp 作为底层引擎。
• 优势:连接池、透明 GZIP、拦截器链、WebSocket 支持都交给 OkHttp,稳定且可定制。

c. Converter(转换器)
• JSON 序列化/反序列化通过 ConverterFactory 插件化:常见的 Gson、Moshi、Simple XML 等。
• 在构建 Retrofit 时指定 converter,框架就知道把请求体对象转成 JSON,也能把响应 JSON 转回你的数据类。

d. CallAdapter(调用适配器)
• 默认返回 Call<T>,需要手动执行 enqueue 或 execute
• 通过添加 RxJava2/3、Kotlin Coroutines、LiveData、Flow 等 CallAdapter,能把接口方法直接声明成 Single<T>Deferred<T>LiveData<T>Flow<T> 等,更契合项目的异步模型。

  1. 灵活扩展与最佳实践
    a. 全局配置
    • 在 Retrofit.Builder 上统一配置 Base URL、超时、拦截器(身份认证、重试策略、日志)等;
    • 对外提供一个单例或注入的 service 实例,避免多次创建。

b. 拦截器与网络策略
• OkHttp 拦截器分为 Application 和 Network 两种,可以做统一的 Token 注入、签名、重试、日志、故障注入;
• 现实项目里还会用缓存拦截器(Cache-Control),配合离线缓存策略提升用户体验。

c. 错误处理
• HTTP 错误码、网络异常、JSON 解析失败都要统一封装一个通用的错误模型;
• 用 response.isSuccessful() 判断状态,或在 CallAdapter 里抛出自定义异常;
• 对 401 做自动刷新 Token、对 500 做降级提示、对超时做重试。

d. 测试与 Mock
• 通过 OkHttp 的 MockWebServer 可以搭建本地假接口,验证请求结构、模拟超时或错误码;
• 接口定义和数据模型都是纯 POJO/数据类,无 Android 依赖,单元测试方便。

分辨率 dp px

1.屏幕分辨率、规模和密度
• 分辨率(Resolution)指的是屏幕的物理像素总数,横向×纵向,比如 1080×1920px。
• 屏幕尺寸(Size)指的是对角线长度,比如 5.5″,决定视觉尺寸大小。
• 像素密度(DPI,Dots Per Inch)指的是每英寸里有多少个像素点,常见有 ldpi(120dpi)、mdpi(160dpi)、hdpi(240dpi)、xhdpi(320dpi)、xxhdpi(480dpi)……

2.px(Pixel,物理像素)
• 真实硬件像素单位,1px 就是屏幕上一个发光点。
• 优点:精确对应硬件,画位图、做像素级特效时用它最直接。
• 缺点:不同 DPI 的设备上,1px 在屏幕上物理大小不一致——高密度设备上更小、低密度设备上更大,导致 UI 元素实际占据的物理尺寸千差万别。

3.dp(Density‑independent Pixel,逻辑像素)
• Android 规定:在 mdpi(160dpi)设备上,1dp 恰好等于 1px;在其他密度设备上,系统会按比例缩放:
px = dp × (dpi / 160)
举例:在 xhdpi(320dpi)设备,dpi/160=2,1dp=2px;在 hdpi(240dpi)时,1dp=1.5px。
• 优点:以“160dpi 基准”做抽象,任何设备上用同样的 dp 值定义布局元素都能保持近似一样的物理大小。
• 使用场景:布局里你看到的所有宽高、边距、字体大小(sp 实质上就是带可调密度的 dp)几乎都推荐用 dp;只有做绘图、动画或直接操作 Bitmap 时,才会用 px 去做精确计算。

4.为什么要区分 dp 和 px?
• 适配是一切:不同分辨率、不同屏幕尺寸、不同 DPI 的设备上,用户操作区域要可触、视觉要一致,就不能硬写 px。
• 资源分类:我们在 res/drawable-mdpi、-hdpi、-xhdpi、-xxhdpi 放置不同像素密度的图,这些图片在运行时也会结合设备 DPI 自动选取,保证清晰度和尺寸一致性。

公钥和私钥

  1. 概念和区别
    • 非对称加密:一对密钥——公钥(Public Key)和私钥(Private Key)
    • 私钥:绝对保密,只能自己持有;
    • 公钥:可以公开分发,任何人都能拿去使用。
    • 核心区别:由私钥可以推导出公钥,但从公钥无法反推出私钥(基于大数分解/椭圆曲线离散对数等数学难题)。

  2. 两种主要用途
    a) 加密解密
    – 客户端或第三方用公钥加密,只有持有私钥的一方才能解密,保证数据传输的机密性;
    – 场景举例:App 拿到服务端公钥后,用它加密用户敏感信息(如登录凭证),服务端用私钥解密。
    b) 数字签名
    – 持有私钥的一方对消息做签名(生成摘要并加密),验证方用公钥解签,保证消息未被篡改且来源可信;
    – 场景举例:服务端在 JWT 中用私钥对 payload 签名,客户端或其它服务通过公钥校验签名,确认数据没被中间人修改。

  3. 常见算法和权衡
    • RSA:基于大数分解,密钥长度通常 2048-bit 或更高;通用、兼容性好,但私钥操作(签名/解密)相对较慢。
    • ECC(椭圆曲线加密,如 ECDSA、ECIES):同等安全强度下密钥更短(256-bit),运算更快,适合移动端场景。
    • 性能考量:
    – 非对称加解密速度比对称慢十几倍以上,通常只用于传输对称密钥或做签名;
    – 大数据量传输时,先用对称加密(如 AES)加密数据,再用非对称加密加密对称密钥,兼顾性能和安全。

线程池组成部分

一、线程池的组成及原理

  1. 四大核心要素
  1. 核心线程数(corePoolSize)
    – 池里保持的最小线程数,即使空闲也不销毁(除非开启了允许核心线程超时)。
    – 作用是应对稳定负载,不用反复创建/销毁线程。
  2. 最大线程数(maximumPoolSize)
    – 池里能容纳的最大线程数。
    – 当核心线程都在跑任务且队列满时,才会继续扩容到此值,防止瞬时峰值导致无限开新线程。
  3. 任务队列(workQueue)
    – 存放等待执行的任务:常用的有无界队列(LinkedBlockingQueue)、有界队列(ArrayBlockingQueue)、直接交付队列(SynchronousQueue)。
    – 策略不同会影响行为:无界队列会让最大线程数失效;SynchronousQueue 则核心线程也满时,立即新建线程或拒绝。
  4. 拒绝策略(RejectedExecutionHandler)
    – 池满且队列也满时如何处理新任务:常见有抛异常、丢弃最老/最新任务、由调用线程自己执行。
    – 我在高稳定性场景通常选 CallerRunsPolicy,让调度线程也跑任务,缓解一下峰值压力。
  1. 线程回收与生命周期
    – 空闲线程在超时(keepAliveTime)后会被终止回收;
    – 配合 allowCoreThreadTimeOut(true) 可以让核心线程也超时回收,用于长时间低负载时释放资源。

  2. 底层执行流程(简述)

  3. 提交任务时,先看当前线程数是否小于 core;
  4. 若 < core,则创建新线程执行;
  5. ≥ core,则先尝试放入队列;
  6. 队列满且当前线程数 < maximum,则再创建线程;
  7. 队列满且线程数 ≥ maximum,则走拒绝策略。
  8. 实战选型建议
    – CPU 密集型:固定大小的线程池(FixedThreadPool),核心=最大≈CPU 核心数;
    – I/O 密集型:可适当增大最大线程,或用 CachedThreadPool(SynchronousQueue);
    – 场景隔离:按业务类型或优先级建不同池,避免互相排队或互相影响。

安卓怎么保存本地数据

  1. 轻量级配置:SharedPreferences / DataStore(Preferences)
    • 场景:存储少量 key‑value,比如用户设置、开关状态、Token、界面显示偏好。
    • SharedPreferences:
    – 优点:API 简单,随用随取;支持 apply()(异步写入)和 commit()(同步写入并返回结果)。
    – 缺点:基于单线程文件锁,写多了会堵主线程,也没有类型安全和流式更新。
    • Jetpack DataStore—Preferences:
    – 用 Kotlin Coroutines + Flow,异步读写、无死锁风险,支持事务操作;
    – 对比旧 API,改用 DataStore 后不用担心 ANR,也能以流的形式在 ViewModel 里直接 collect 更新。

  2. 结构化关系数据:SQLite / Room
    • 场景:复杂表结构、多表关联、事务、分页、联查。
    • 原生 SQLite + SQLiteOpenHelper:
    – 优点:轻量,无额外依赖;
    – 缺点:要手写建表脚本、Cursor 操作、手动管理事务,维护成本高。
    • Room(Jetpack):
    – 注解 Entity/DAO,编译期校验 SQL,自动生成 CRUD 代码;
    – 原生支持 Coroutines/Flow、LiveData,也兼容 RxJava;
    – 实战中,我用 Room 完成过联表分页、复杂事务,代码可读性和安全性都大幅提升。

  3. 文件存储:Internal / External / EncryptedFile
    • 场景:存音视频、下载缓存、日志、临时文件。
    • 内部存储(Internal):私有目录,不需要权限;
    • 外部存储(External):可共享但需动态申请存储权限(Android 11+ 更多限制);
    • Jetpack Security—EncryptedFile:
    – 内部文件加密读写,自动管理密钥,敏感日志或用户数据落盘更安全。

  4. 高性能键值:MMKV、Hawk
    • 场景:替代 SharedPreferences,需要非常快的读写和高并发场景;
    • MMKV(字节对齐、 mmap 加速)在我做过性能攻坚时,把热点配置从 SharedPreferences 全部迁移到 MMKV,启动性能和响应速度都有明显提升。

  5. 混合方案:对称+非对称加密、缓存策略
    • 大数据传输或云端下发配置:
    – 本地先用 AES(对称)加密文件或 JSON 数据,再用公钥(RSA/ECC)加密对称密钥,保证安全且性能最优;
    • 缓存:
    – HTTP 缓存(OkHttp Cache)或 WorkManager + Room 实现定时同步本地缓存,既保证离线可用,又不让数据库膨胀。

内存泄漏怎么排查

“内存泄漏排查主要就是定位哪些对象没有被及时回收,以及为什么一直持有引用。首先,我会利用 Android Studio 提供的 Memory Profiler,对应用进行实时监控和 Heap Dump 分析。通过观察内存使用的趋势,能快速判断是否存在泄漏。

接下来,我通常会用 Heap Dump 工具抓取内存快照,然后借助工具(例如 MAT 或者 Android Studio 自带的分析功能)查看 dominator tree,定位哪些对象占用了大量内存。重点是关注 Activity 或 Fragment 是否在销毁后依然存在;常见原因包括匿名内部类或非静态内部类在持有外部类引用而导致泄漏,还有单例、静态变量或者未注销的监听器问题。

此外,我也会借助 LeakCanary 这样的第三方库,在开发过程中实时监测内存泄漏,它能够自动检测到泄露并给出泄露链路,帮助定位是某个 Context、View等没有被正确释放。

在排查过程中,我会结合业务逻辑和代码审查,比如细查集合、缓存的使用,确保用完后及时清空、移除监听,避免对象的生命周期长于所在组件。最后,我还会通过多次进行场景重现、逐步关闭部分功能来缩小范围,从而准确锁定问题所在。

总结一下,排查内存泄漏其实就是一个流程:监控内存使用趋势→获取 Heap Dump 快照→利用工具分析保留树和泄漏链路→结合代码和业务逻辑进行排查,从而确定问题并着手解决。这样不仅能找出泄漏根源,还能在后续优化代码架构时有针对性地做改进。”

APP的页面突然卡了一下,怎么排查

“首先,当页面突然卡住,我会先判断是偶发还是持续的问题,然后按以下步骤排查:

  1. 首先查看Logcat,确认是否有异常、ANR或者异常的GC日志。通过这些日志,通常能发现是否是某个线程阻塞或者内存过度回收导致的问题。

  2. 检查主线程加载情况。由于Android的UI操作全在主线程上,如果有耗时操作(如网络请求、数据库操作或复杂计算)在主线程执行,就容易导致卡顿。此时,我会回顾相关逻辑,确保有异步操作或者使用Coroutine、Thread等处理耗时任务。

  3. 利用Profiler工具或者Systrace进一步分析。通过内置的Android Studio Profiler,查看CPU加载情况、内存情况和帧率监控,确定问题是否来自于UI绘制、布局解析过于复杂或者动画操作过重。

  4. 检查页面布局和渲染。由于嵌套过深或频繁的自定义绘制也可能导致页面卡顿,我会审查布局文件,必要时借助Hierarchy Viewer或者Layout Inspector查看是否存在性能瓶颈。

  5. 排查内存泄漏或者垃圾回收频繁。频繁GC也会导致瞬间卡顿,通常我会结合Profiler进行内存分析,确认是否有对象没有及时回收造成内存紧张。

综上,我的排查流程大致是:先查看日志,然后利用Profiler或者Systrace,最后根据定位信息对耗时操作、布局复杂度或者不当的线程操作进行优化和修复。这样能较快地找到卡顿的根源并进行有针对性的改进。”

ANR怎么排查,定义是什么

“ANR,全称是 Application Not Responding,也就是应用无响应。当主线程超过规定时间(通常是5秒)未能响应用户输入或者系统消息时,就会触发 ANR 弹窗,让用户选择关闭或者等待。

关于排查 ANR,我主要会按照以下步骤进行:

  1. 首先通过 Logcat 查看相关日志。系统日志会记录 ANR 发生时的关键信息,尤其是主线程的调用栈,这能帮我迅速定位导致阻塞的代码段。

  2. 接着,我会分析系统生成的 traces.txt 文件,这个文件一般位于 /data/anr/ 目录下。通过查看这里的堆栈信息,可以确定是否由于长时间占用主线程的耗时操作,比如网络请求、数据库读写或者复杂运算导致的。

  3. 在确定问题区域后,进一步审查业务代码,确保耗时任务都能在子线程或通过异步处理来完成。也会结合使用 Systrace 或 Android Studio 的 Profiler 工具,监控 UI 主线程的调度情况,看是否存在频繁的布局重绘或者其他阻塞性的操作。

  4. 同时,也会检查是否存在因为不当使用同步或锁等机制导致的死锁或者竞争条件,这些情况都可能造成 ANR。

整体思路就是先通过日志和 traces.txt 定位主线程被阻塞的位置,然后结合代码审查和调试工具细致分析原因,最后针对性地优化或改写耗时操作。这样就能有效避免和解决 ANR 问题。”

使用过哪些开源库

比如说,我经常用 Retrofit 来处理网络请求,结合 OkHttp,进一步提升请求的稳定性和效率;而为了处理 JSON 数据,我通常会搭配 Gson 或 Moshi,这样解析和序列化变得特别方便。

另外,在图片加载方面,我常用 Glide 来快速异步加载图片,并通过它的缓存机制改善用户体验。对于异步编程,我不仅使用 RxJava 在响应式编程中灵活处理数据流,还熟悉 Kotlin Coroutines,它让代码更简洁直观,并且通过挂起函数有效替代回调。此外,为了项目中更好地解耦和管理依赖,我也使用过 Dagger 进行依赖注入,它能大大提高代码的可测试性和扩展性。

在调试和优化方面,我会用 LeakCanary 来监控潜在的内存泄漏问题,确保应用长期运行稳定。还有 Timber,这个日志库帮助我快速定位问题,特别是在多线程和异步的场景下更能体现它的优势。

对OKHTTP的了解然后这个框架的设计怎么样

“我对 OkHttp 的理解主要是它作为一个高效、轻量级的 HTTP 客户端,在 Android 和 Java 开发中都获得了很广泛的应用。它的设计非常现代化,目标是提高网络请求的性能和稳定性。

首先,OkHttp 在设计上非常注重连接复用和资源管理。它通过连接池来管理 HTTP/1.1 以及支持 HTTP/2 连接,同一连接可以被多个请求共享,这样能大幅度降低建立连接的开销。同时,它还实现了透明的 GZIP 压缩和响应缓存,从而在网络条件较差的情况下也能提升响应速度和降低流量消耗。

其次,OkHttp 的拦截器链设计非常出色。拦截器机制不仅让我们在请求和响应中进行灵活的预处理和后处理,比如添加公共请求头、统一处理日志和异常,还能实现自定义重试策略。这种责任链模式使得框架扩展性强,同时也比较容易维护和调试。

此外,OkHttp 对异步请求的支持也非常成熟。通过回调机制以及和 Kotlin 协程等方式结合使用,可以以简单且高效的方式对网络请求进行异步调用,跟传统的方式相比,没有那么多回调地狱的问题,而且扩展性也很好。

整体而言,OkHttp 的设计遵循了高内聚低耦合的原则,把网络请求的各个关注点拆分成多个模块,比如连接管理、拦截链、缓存及重试机制等。它既能满足基础的网络请求需求,还能灵活应对复杂的网络环境。

Databinding有哪些了解

“DataBinding 是 Android 提供的一个框架,通过将布局 XML 文件中的 UI 组件与数据模型直接绑定,实现数据和视图的自动同步。这样一来,我们就不需要在代码里频繁调用 findViewById 来手动查找控件,也能更清晰地维护 UI 与业务逻辑之间的映射关系。

第一,它解决了重复查找控件的问题。以前需要手动调用 findViewById,而使用 DataBinding 后,系统会自动生成绑定类,提供直接的属性访问,这不仅减少了代码量,也避免了类型转换的问题,提高了类型安全性。

第二,DataBinding 让布局文件和逻辑代码之间的联系更加直观。在布局文件中,我们可以通过表达式绑定数据,这样更容易实现数据与视图的同步更新,能够保持代码逻辑的清晰和整洁。当数据变化时,可以自动更新界面,从而降低了逻辑与表现层间的耦合度。

第三,DataBinding 还支持双向绑定,尤其在需要用户输入反馈时非常有用。双向绑定可以保证当数据发生变化时,界面自动刷新,同时用户的输入也能直接反馈到数据层,减少了手动编写监听器的繁琐工作。

此外,在配合 MVVM 架构中,DataBinding 能够很好地与 ViewModel 配合,将业务逻辑进一步抽离到数据层,提升代码的可测试性和维护性。当然,使用过程中也可能会遇到编译速度的问题或者调试表达式时的复杂性,但总体上我觉得 DataBinding 让开发变得更加高效、代码更加整洁。

RelativeLayout 和 LinearLayout 怎么选, 为什么

“在选择 RelativeLayout 和 LinearLayout 时,我通常会根据具体的布局需求来判断。LinearLayout 的优势在于布局比较简单,一般只需要水平或垂直排列一组连续控件时,用它就比较直接,性能开销也较小,因为它只是简单地一层排序,而不会对每个控件进行复杂的重新计算和定位。

而 RelativeLayout 则适合需要控件之间复杂对齐或相对位置关系的场景。它能更灵活地让控件相对于父布局或其他控件定位,从而在不需要嵌套太多布局的情况下实现复杂的 UI。但代价是可能会多次测量和布局计算,稍微复杂一点的层级和计算可能会影响性能,尤其在深层嵌套时会比较吃力。

所以,选择时我会考虑两个因素:一是布局的复杂程度,简单的排列用 LinearLayout 就足够了;二是性能优化。如果布局过于复杂,可以考虑通过减少布局层级(比如用 RelativeLayout 代替多个嵌套的 LinearLayout)来优化性能。总的来说,每个控件的数量和具体需求会影响我的选择,一定要平衡清晰的布局结构和性能。”

自定义Layout主要有哪些流程

“在自定义布局的过程中,我通常会按照几个主要流程来进行开发。首先,你需要明确需求:确定你自定义布局的目的是为了满足什么样的排版或者交互效果,比如是否需要打破现有布局的限制、实现特殊的排列逻辑或者动态调整子View的位置和大小。

第一步是继承合适的类。如果只是单纯实现一个不包含子View的控件,一般继承 View 即可;但如果是需要容纳多个子控件的那种,就需要继承 ViewGroup。继承之后,通常在构造函数中获取自定义属性,这样开发者就可以通过 XML 配置一些排版规则或者属性值。

第二个关键流程是测量阶段,即重写 onMeasure 方法。在这里,主要是对 View 自身和所有子View进行测量。你需要分析传进来的测量模式(MeasureSpec),根据自定义布局的需求来决定每个子控件的大小和位置。这个阶段非常重要,因为它直接影响到布局的最终显示效果和性能,必须确保测量的结果能满足动态内容变化和不同屏幕尺寸的兼容。

接下来是布局阶段,也就是重写 onLayout 方法。在这个方法中,拿到上一步中测量好的宽高信息后,你需要根据自定义的排列规则为每个子控件确定它们的具体坐标位置。此时要注意处理好内边距、间距等因素,还可能需要考虑子控件之间的叠加或重叠问题。正确的布局逻辑能确保无论内容如何变化,都能平稳、合理地展示。

此外,虽然不是所有自定义布局都需要重写 onDraw,但如果你的自定义布局有特殊的绘制需求,比如背景绘制或装饰效果,可能会选择重写 onDraw 方法,这样可以在布局绘制完成后,在画布上进行一些额外的渲染。

最后,调试和优化也是关键环节。自定义布局在处理复杂排列时可能会涉及多次测量和布局,容易导致性能瓶颈。所以在开发过程中,我会用 Profiler 和其它调试工具来观察性能表现,观察布局是否存在过多的重绘或者节点层级过深的问题,必要的时候进行优化,比如减少不必要的嵌套或者预先测量计算。

总的来说,自定义布局的主要流程有:明确需求和继承合适的类,解析自定义属性;进入测量阶段重写 onMeasure,根据子View的尺寸需求及排版规则计算好尺寸;紧接着是布局阶段重写 onLayout,对每个子控件进行精准定位;然后如果需要处理自定义绘制,就在 onDraw 中进行额外的绘制工作;最后,对整个流程进行调试、测试和性能的优化。

滑动过程卡顿,刷新率过低,怎么排查

“在遇到滑动过程卡顿,以及刷新率过低的问题时,我通常会从以下几个方面展开排查:

首先,我会观察问题出现的场景,判断是在特定页面还是所有页面都有这种情况,这样可以帮助缩小范围。针对页面滑动时的卡顿,首先要检查是否存在耗时操作在主线程中执行,比如在滑动过程中有复杂计算、频繁的 UI 刷新或者额外的数据加载。

接下来,我会使用 Android Studio Profiler 来监控 CPU 和 GPU 的使用情况。通过抓取帧率数据,我们可以明确是否由于处理复杂视图渲染或动画导致的性能瓶颈。观察 GPU 渲染的时间是否超过 16 毫秒(对应每秒60帧),如果超过,就说明在图形渲染上存在问题。

同时,我也会查看 Logcat 日志,排查是否有异常或者频繁的垃圾回收现象,因为过于频繁的 GC 会导致滑动过程中突然停顿。另外,我还会利用 Systrace 工具来详细记录滑动过程中的各个线程的执行情况,包括主线程的布局、绘制和事件分发等环节。如果某个环节的执行时间偏长,就需要进一步分析原因。

此外,我会检查布局文件中是否存在层级过深或者布局嵌套过多的问题,这样会给系统带来额外的计算和绘制开销。适当简化布局结构,或者尽量减少不必要的嵌套可能有助于提高流畅度。另外,自定义 View、动画效果以及Bitmap资源大小是否合理也是需要重点关注的点。

最后,如果页面中有涉及到 RecyclerView 或 ListView 的话,还要关注 ViewHolder 的回收和复用情况。错误的布局重绘或者频繁更新数据源也会影响整体的滑动流畅度。根据这些排查思路,逐步定位是业务逻辑问题、布局复杂度问题还是渲染性能问题,然后对症下药进行优化。

写几个synchronized的用法

class Example1 {

    // 使用 @Synchronized 注解表示该方法在执行时会锁定当前对象(this)
    /*
    @Synchronized 是 Kotlin 中的一个注解,用来标记一个函数,使得该函数在执行时会被自动加锁
    相当于给整个方法加上 synchronized 关键字。它和 Java 中的 synchronized 方法类似
    保证同一时刻只有一个线程可以执行这个函数,有助于实现线程安全。
     */
    @Synchronized
    fun synchronizedMethod() {
        println("This is a synchronized method in Kotlin.")
    }

    // 使用 synchronized 块,可以灵活选择锁对象
    /*
    在 synchronized(this) 这个写法中,this 指的是当前对象实例
    也就是说,当进入这个 synchronized 块时,会对当前对象获取锁,确保在同一时刻只有一个线程能执行该代码块
    这样可以避免多个线程同时访问和修改共享数据时产生冲突
    如果在同一个实例上有多个 synchronized(this) 块,它们之间会因为使用同一个锁而互斥执行。
     */
    fun blockSynchronized() {
        synchronized(this) {
            println("This is a synchronized block using 'this' as lock.")
        }
    }
}

fun main() {
    val example = Example1()
    example.synchronizedMethod()
    example.blockSynchronized()
}
class Example2 {
    // 自定义锁对象,常用于不同同步代码块之间互不干扰
    /*
    Any 是 Kotlin 中所有类的根基类,类似于 Java 中的 Object。

    使用 this 作为锁对象,会将整个对象实例暴露给外部
    如果其他代码不小心也使用相同的对象做锁,就可能造成额外的锁竞争,甚至导致死锁。

    如果类中多个方法或者代码块都使用 this 作为锁对象,这样不同的方法之间也会互相阻塞,显得粒度过粗,不够精细
    相比之下,使用自定义的锁对象(比如 private val lock = Any())
    可以让我们只对需要同步的那段代码进行加锁,避免对整个对象的其他业务逻辑产生影响。
     */
    private val lock = Any()

    fun methodWithCustomLock() {
        synchronized(lock) {
            println("This is a synchronized block with a custom lock in Kotlin.")
        }
    }
}

fun main() {
    val example = Example2()
    example.methodWithCustomLock()
}
  1. 在 Example1 中,有两种用法:

    • @Synchronized 注解
      这个是在 Kotlin 中的一种语法糖,用于标记方法。它作用于整个方法,在进入方法前自动对当前实例(this)进行加锁,确保同一时刻只有一个线程能执行该方法,就像在 Java 中用 synchronized 修饰方法一样。它简单明了,适用于需要整个方法线程安全的场景。

    • synchronized(this) 代码块
      这里使用 synchronized 块,并显式传入了 this 作为锁对象。这意味着进入该代码块时,将对当前对象实例加锁。与 @Synchronized 注解类似,此处也是锁定当前对象,只不过可以让你灵活选择锁定的范围。如果只需要保护代码块而非整个方法,就可以采用这种方式。

  2. 在 Example2 中,使用了自定义锁对象(private val lock = Any())和 synchronized 块结合的方式。
    通过传入自定义锁对象 lock 作为同步锁,可以让多个 synchronized 块之间只共享这个特定的锁,而不会和其他加锁的部分(比如使用 this 的锁)产生干扰。这样做的好处是,可以更精细地控制同步的粒度,防止不必要的锁竞争和潜在的死锁问题。特别是当同一个类中有多个需要独立同步的代码块时,为每个代码块分别定义不同的锁对象会更安全,更灵活一些。

类加载器

“对于类加载器,我理解它主要负责将字节码文件(.class 文件)从磁盘或其它存储介质加载到内存中,并生成 Class 对象,这个过程涉及到加载、连接和初始化三个阶段。

先从层级结构来说,Java 的类加载器主要包括三种:启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)和应用类加载器(Application ClassLoader)。启动类加载器用 C++ 编写,负责加载核心库,比如 rt.jar 中的类,扩展类加载器则处理 JRE/lib/ext 目录中的扩展类库,而应用类加载器则是开发者常用的,用于加载我们应用程序所在的 classpath 下的类。

再具体讲下整个过程:

  1. 在加载阶段,类加载器从某个来源(可能是本地文件系统、网络或者其它数据源)读取类的二进制数据,并将其转化为方法区的内存数据结构。这时候还不会进行初始化,只是为连接阶段做好准备。

  2. 在连接阶段,又分为验证、准备和解析。其中验证是检查字节流符合 JVM 的要求,防止恶意字节码;准备阶段为类变量分配内存,设置默认初始值;解析阶段则负责将符号引用转换为直接引用。

  3. 初始化阶段会对类变量进行初始化(比如静态代码块和静态变量的赋值)。这时候类加载器会按照一定的顺序加载父类、实现接口以及成员变量,确保整个类的结构和依赖被正确构建。

说到 Android 平台上,虽然原理和标准 JDK 基本保持一致,但有一些不同点。Android 的运行时(比如 ART 或 Dalvik)采用了自己的类加载机制和优化策略,比如能够针对 Android 应用 的多 dex 文件和资源加载进行一定的优化,这也是我们在 Android 开发过程中要特别关注的问题。

此外,类加载器之间也支持双亲委派模型。当一个类加载器接收到加载请求后,会先委托给父加载器,如果父加载器无法加载,再由当前加载器尝试加载。这样做既能保证核心类的一致性,又能避免重复加载和类冲突。

最后,类加载器除了基础的类加载功能,还可以用于实现一些高级特性,比如动态代理、模块化加载或热更新机制。掌握类加载器的工作原理,有助于我们在遇到 ClassNotFoundException 或 NoClassDefFoundError 时更深入定位问题,同时对于理解 Java 的反射和动态生成代码也是非常有帮助的。”

场景题:如何加载大文件

  1. 明确场景和瓶颈
    – 是文本文件还是二进制(视频、音频、数据库、JSON、CSV、地图切片)?
    – 加载到内存里一次性处理会不会 OOM?能不能用流式处理?
    – 屏幕上需要一次显示全量还是分页/滑动时按需加载?

  2. 流式读取,避免一次性分配
    – 对于大型文本/JSON/CSV,用 InputStream + BufferedReader(或 JsonReader)一边读一边处理,按行或按块解析,永不把整个文件读入 String 或对象列表。
    – 二进制文件(图片、音视频)也类似,通过 FileInputStream + byte[ ] 缓冲区读取若干 KB,每读一块就处理或交给下游管道(解码、渲染、写缓存)。

  3. 内存映射(Memory‑map)
    – 对于超大文件、高性能需求场景,可以用 FileChannel.map() 得到 MappedByteBuffer,把文件映射到虚拟内存。
    – 优点:操作系统帮你做页级加载和回收,不管文件多大,都只是把真正访问的页带到内存,既省时又省空间。

  4. 按需分页/分片加载
    – 如果要在列表里显示文件内容(比如日志浏览器、大规模表格、地图切片),不一次性加载所有数据;
    – 结合 RecyclerView + PagingLibrary,把数据源抽象成 DataSource,用户滚动到哪儿就读哪儿,后台再 pre‑fetch。
    – 对于视频/音频,同理用 ExoPlayer/MediaPlayer 支持流式、断点续播。

  5. 后台线程与生命周期管理
    – 文件 IO 和解析一定要放在 IO 线程(Coroutine IO Dispatcher、RxJava Schedulers.io()、ExecutorService),避免卡主 UI;
    – 使用 LifecycleScope/WorkManager/Service 做长任务时,注意生命周期管理和进程被杀后如何恢复(checkpoint、断点续读)。

  6. 缓存与断点续读
    – 对远程大文件,先用 OkHttp + Range 请求做分段下载,把已下载部分存本地;
    – 断点续载:记录已下载字节偏移,下次启动或断网重连时,从上次位置继续,结合 RandomAccessFile 或 Range header。

  7. 体验优化
    – 加载进度实时反馈:UI 端根据读字节数/总大小计算百分比,展示进度条或“正在加载第 N 行”;
    – 限速/节流:防止持续读写让磁盘和 CPU 过热,或在低电量模式下自动降低读取频率。

总结一句话:大文件加载的核心是“流式+分片+后台”,绝不“一次性把整个文件拉进内存”,并结合分页架构与断点续载策略,既保证性能又给用户流畅的体验。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值