当前位置: 首页 > article >正文

腾讯阅文集团Android面试题及参考答案

Java 的基本数据类型有哪些?分别简述一下。

Java 的基本数据类型共有 8 种,可分为 4 类:整数类型、浮点类型、字符类型和布尔类型 。

  • 整数类型:包括 byte、short、int 和 long。byte 占 1 个字节,取值范围是 - 128 到 127,适用于存储一些小范围的整数,如文件流中的字节数据。short 占 2 个字节,范围是 - 32768 到 32767,在某些特定的嵌入式系统或对内存要求苛刻且数据范围不大的场景中使用。int 是最常用的整数类型,占 4 个字节,范围为 - 2147483648 到 2147483647,一般用于表示常规的整数数据,如数组的下标、循环计数器等。long 占 8 个字节,能表示更大范围的整数,当需要处理超出 int 范围的整数时使用,其值后面需加 L 或 l 后缀。

  • 浮点类型:有 float 和 double。float 占 4 个字节,遵循 IEEE 754 标准,可表示单精度浮点数,有效数字约为 6 到 7 位,适用于对精度要求不高的浮点数运算,其值后面需加 F 或 f 后缀。double 占 8 个字节,能表示双精度浮点数,有效数字约为 15 位左右,是 Java 中默认的浮点数类型,通常用于科学计算、金融等对精度要求较高的领域。

  • 字符类型:char 占 2 个字节,用于存储单个字符,如字母、数字、标点符号等,其值用单引号括起来,可存储 Unicode 字符集中的任意字符。

  • 布尔类型:boolean 只有 true 和 false 两个值,占 1 个字节,用于表示逻辑判断的结果,常用于条件判断和循环控制等语句中 。

请详细说明 string、stringbuffer、stringbuilder 的区别。

  • 可变与不可变

    • String:是不可变的字符序列。一旦创建,其值就不能被改变。例如,当对一个 String 对象进行拼接操作时,实际上是创建了一个新的 String 对象,而原来的对象并没有改变。如 String s1 = "Hello"; String s2 = s1 + " World"; 这里的 s1 仍然是 "Hello",而 s2 是新创建的 "Hello World"。
    • StringBuffer 和 StringBuilder:都是可变的字符序列。它们在进行字符串操作时,是在原对象上进行修改,而不是创建新的对象。比如使用 StringBuffer 进行字符串拼接时,会直接在原有的 StringBuffer 对象上添加新的字符。
  • 线程安全性

    • StringBuffer:是线程安全的,其内部的方法大多是被 synchronized 关键字修饰的,在多线程环境下可以保证数据的一致性和正确性。
    • StringBuilder:是非线程安全的,它的性能比 StringBuffer 要高,因为它没有加锁的开销。所以在单线程环境下,优先考虑使用 StringBuilder 进行字符串的操作。
  • 性能表现

    • String:由于其不可变性,在频繁进行字符串拼接等操作时,会创建大量的临时对象,导致性能下降。
    • StringBuffer:虽然是线程安全的,但由于加锁机制,在性能上会有一定的损耗,尤其是在单线程环境下。
    • StringBuilder:在单线程环境下进行字符串操作时,性能最好,因为它既不需要创建大量临时对象,也没有加锁的开销。

阐述抽象类和接口的区别。

  • 定义和语法
    • 抽象类:使用 abstract 关键字修饰的类,其中可以包含抽象方法和非抽象方法。抽象方法只有方法签名,没有方法体,需要在子类中被实现。例如:

abstract class Animal {
    abstract void makeSound();
    void eat() {
        System.out.println("Animal is eating");
    }
}

  • 接口:使用 interface 关键字定义,接口中的方法默认都是 public abstract 的,变量默认都是 public static final 的,接口中不能包含普通的成员变量和非抽象的方法体。例如:

interface Flyable {
    void fly();
}

  • 实现方式

    • 抽象类:通过继承来实现,一个类只能继承一个抽象类。子类必须实现抽象类中的所有抽象方法,除非子类也是抽象类。
    • 接口:通过实现来完成,一个类可以实现多个接口。实现类必须实现接口中的所有方法。
  • 设计目的

    • 抽象类:主要用于对一组具有相似特征和行为的事物进行抽象和概括,它提供了一种模板式的设计,既可以定义通用的行为和属性,也可以为子类提供一些默认的实现,便于代码的复用和扩展。
    • 接口:更侧重于定义一组规范和契约,用于规定实现类必须具备的行为和功能,而不关注具体的实现细节。它常用于定义不同类之间的协作和交互的标准。
  • 功能特性

    • 抽象类:可以有构造方法,在实例化子类时,会先调用抽象类的构造方法来初始化抽象类的成员变量。抽象类中可以包含各种访问修饰符的成员变量和方法,用于在类的继承体系中传递信息和实现不同层次的功能。
    • 接口:不能有构造方法,因为接口本身不能被实例化。接口中的方法只能是 public abstract ,变量只能是 public static final ,这保证了接口的规范性和一致性,使得不同的实现类可以遵循相同的标准进行实现。

解释 Java 线程池的概念、原理以及信号量的相关知识。

  • 线程池的概念

    • 线程池是一种多线程处理形式,它预先创建了一定数量的线程,并将这些线程保存在一个池中,当有任务需要处理时,直接从线程池中获取线程来执行任务,任务执行完毕后,线程并不会被销毁,而是回到线程池中等待下一次任务的分配。这样可以避免频繁地创建和销毁线程所带来的开销,提高系统的性能和资源利用率。
  • 线程池的原理

    • 主要由三部分组成:线程池管理器、工作线程和任务队列。线程池管理器负责创建、管理和销毁线程池中的线程,它根据系统的配置和任务的负载情况来动态调整线程池的大小。工作线程是线程池中实际执行任务的线程,它们从任务队列中获取任务并执行。任务队列用于存储等待执行的任务,当线程池中的线程有空闲时,就会从任务队列中取出一个任务进行执行。
    • 当向线程池提交一个任务时,线程池首先会判断当前线程池中的线程数量是否达到了核心线程数,如果没有达到,则创建一个新的线程来执行任务;如果已经达到了核心线程数,则将任务放入任务队列中等待执行。当任务队列已满且线程池中的线程数量未达到最大线程数时,线程池会创建新的非核心线程来执行任务;当线程池中的线程数量达到最大线程数且任务队列已满时,则根据线程池的饱和策略来处理新提交的任务,如拒绝任务或阻塞等待等。
  • 信号量的相关知识

    • 信号量是一种用于控制多个线程对共享资源访问的机制,它可以维护一个计数器,表示当前可用的资源数量。线程在访问共享资源之前,必须先获取信号量,如果信号量的计数器大于 0,则表示有可用资源,线程可以获取信号量并将计数器减 1,然后访问共享资源;当线程访问完共享资源后,必须释放信号量,将计数器加 1,以便其他线程可以获取信号量并访问共享资源。
    • 信号量主要有两个操作:acquire 和 release 。acquire 操作用于获取信号量,如果信号量的计数器为 0,则线程会被阻塞,直到有其他线程释放信号量;release 操作用于释放信号量,将信号量的计数器加 1,并唤醒等待该信号量的线程。
    • 信号量常用于实现资源的并发访问控制、线程的同步和协作等场景。例如,在生产者 - 消费者问题中,可以使用信号量来控制生产者和消费者对缓冲区的访问,确保在缓冲区满时生产者等待,缓冲区空时消费者等待。

详述 HashMap 的结构、扩容机制(包括扩容参数)及其原理,说明它是否线程安全。

  • HashMap 的结构

    • 数组:HashMap 内部存储数据的基础结构是一个数组,数组的每个元素称为桶(bucket)。每个桶可以存储一个或多个键值对,当有键值对要存储时,会根据键的哈希值计算出它在数组中的位置,然后将键值对存储在该位置对应的桶中。
    • 链表或红黑树:当多个键值对的哈希值计算出的位置相同时,它们会以链表的形式存储在同一个桶中。从 Java 8 开始,当链表的长度超过一定阈值(默认为 8)时,链表会转换为红黑树,以提高查找效率。红黑树是一种自平衡的二叉查找树,它可以保证在最坏情况下,查找、插入和删除操作的时间复杂度都为 O (log n) ,而链表的时间复杂度在最坏情况下为 O (n) 。
  • HashMap 的扩容机制及原理

    • 扩容参数:HashMap 有两个重要的扩容参数,一个是初始容量 initialCapacity ,另一个是加载因子 loadFactor 。初始容量是指 HashMap 在创建时数组的大小,默认值为 16。加载因子是指当 HashMap 中存储的键值对数量达到数组容量乘以加载因子时,就会触发扩容操作,默认的加载因子是 0.75。
    • 扩容原理:当 HashMap 中的元素数量超过了当前数组容量乘以加载因子时,就会进行扩容。扩容时,会创建一个新的数组,其容量通常是原来数组容量的 2 倍。然后,将原来数组中的每个桶中的键值对重新计算哈希值,并根据新的哈希值将它们存储到新的数组中。这个过程称为 rehash。在 rehash 过程中,由于数组的大小发生了变化,键的哈希值对应的桶位置也可能会发生变化,所以需要重新计算每个键值对的存储位置。
  • HashMap 的线程安全性

    • HashMap 不是线程安全的。在多线程环境下,如果多个线程同时对 HashMap 进行读写操作,可能会导致数据不一致和并发问题。例如,当多个线程同时进行 put 操作时,可能会出现同时计算出相同的哈希值,然后同时对同一个桶进行操作,导致数据覆盖或丢失等问题。同样,在进行 get 操作时,如果有线程正在进行 put 或 remove 操作,也可能会导致读取到不一致的数据。
    • 如果要在多线程环境下使用类似 HashMap 的功能,可以使用线程安全的 ConcurrentHashMap ,它采用了锁分段等技术来保证在多线程环境下的并发安全性。

既然 HashMap 线程不安全,那么在需要线程安全的场景下会使用什么?比如 ConcurrentHashMap,解释它的内部实现原理。

在需要线程安全的场景下,Java 中的 ConcurrentHashMap 是非常有用的数据结构。其内部实现相当精巧。

首先,ConcurrentHashMap 将数据存储划分成多个段。每个段就像是一个小型的 HashMap,并且它们可以被独立锁定。这种设计被称为分段锁。当多个线程同时访问不同的段时,它们可以并发操作而不会相互阻塞,这极大地提高了并发访问的效率。

其次,在数据读取过程中,只要段内的数据未被修改,ConcurrentHashMap 就允许无锁并发读取。这减少了加锁的开销,提高了读取操作的性能。

此外,在写入或修改数据时,ConcurrentHashMap 仅锁定正在操作的那个段,而不是整个映射表。并且它使用诸如比较并交换(CAS)等多种技术来确保数据操作的原子性和一致性。

总体而言,通过这些机制,ConcurrentHashMap 能够提供良好的线程安全性和较高的并发性能,使其非常适合在需要频繁并发访问映射表的多线程环境中使用。

谈谈你对多态的理解。

多态是面向对象编程中的重要特性之一。它允许不同类的对象被当作一个公共超类的对象来对待,并且不同的子类对象能够根据其具体实现展现出不同的行为。

多态的一种形式是方法重写。当子类定义了一个与超类中方法签名完全相同的方法时,子类中的这个方法就重写了超类中的方法。在运行时,Java 虚拟机(JVM)根据对象的实际类型,而非引用的类型来决定调用哪个方法的实现。例如,如果我们有一个超类 Animal,它有一个 makeSound () 方法,而子类如 Dog 和 Cat 重写了这个方法以发出它们各自特定的声音,当我们对一个实际类型为 Dog 或 Cat 的 Animal 类型对象调用 makeSound () 方法时,子类中相应的重写方法就会被执行。

另一种形式是方法重载,它发生在同一个类内部。我们可以定义多个同名但参数列表不同的方法。编译器根据传入参数的数量、类型和顺序来决定调用哪个方法。

多态使代码更具灵活性和可扩展性。它允许我们编写更通用且可复用的代码,因为我们可以编写针对超类进行操作的代码,并且它可以在无需事先知道具体类型的情况下与不同的子类一起工作。它还简化了代码的设计和维护,通过添加新的子类来添加新功能而无需修改使用超类的现有代码变得更加容易。

说明线程与进程的区别。

进程是正在执行的计算机程序的一个实例。它拥有自己独立的内存空间,包括代码段、数据段、栈和堆。每个进程在其自身的地址空间中运行,并且进程之间是相互隔离的。它们拥有自己的系统资源,如文件描述符、套接字等。一个进程可以包含多个线程。

而线程是进程内的轻量级子进程。线程与它们所属的进程共享相同的内存空间,包括代码段、代码数据段和堆。然而,每个线程都有自己的栈。线程可以访问和修改进程内的共享数据,这使得线程之间的数据通信和共享相对容易,但也需要适当的同步机制来避免数据冲突。

在执行方面,进程是一个相对独立的执行单元,由操作系统进行调度和管理。当进程切换时,需要保存和恢复整个进程的上下文,包括内存状态、寄存器值等,这会产生相对较大的开销。而线程是进程内更小的执行单元,线程切换的成本远低于进程切换的成本,因为只需要保存和恢复线程自身的栈和少量的上下文信息。

从资源利用的角度来看,创建和销毁进程会消耗更多的系统资源,而创建和销毁线程则相对更轻量级。并且在多线程进程中,多个线程可以并发执行,从而更好地利用多核 CPU 来提高程序的整体效率。

哪种方式进行字符串拼接比较高效?如何进行拼接?

在 Java 中,有几种拼接字符串的方式,它们的效率各不相同。

最直接的方式是使用 “+” 运算符。然而,当在循环中使用 “+” 运算符来拼接大量字符串时,实际上每次都会创建一个新的 String 对象,这会导致大量不必要的对象创建和内存开销,因此效率不高。

一种更好的方式是使用 StringBuilder 或 StringBuffer 类。在单线程环境中,StringBuilder 通常更高效。当我们需要拼接字符串时,我们首先创建一个 StringBuilder 对象,然后使用 append () 方法不断地向它添加字符串。最后,我们可以调用 toString () 方法来获取最终拼接好的字符串。例如:

StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(" ");
sb.append("World");
String result = sb.toString();

StringBuffer 与 StringBuilder 类似,但它是线程安全的。所以如果字符串拼接操作是在多线程环境中,使用 StringBuffer 是更好的选择。其使用方法与 StringBuilder 基本相同,即首先创建一个 StringBuffer 对象,然后使用 append () 方法拼接字符串,最后调用 toString () 方法获取结果。

一般来说,如果是简单且不频繁的字符串拼接,使用 “+” 运算符就可以了。但对于需要拼接大量字符串的场景,尤其是在循环中,使用 StringBuilder 或 StringBuffer 更高效,单线程时选择 StringBuilder,多线程时选择 StringBuffer。

说明在什么情况下 try、catch、finally 语句中的 finally 执行不到。

一般来说,无论 try 块中是否抛出异常,finally 块都会在 try 块执行之后被执行。然而,存在一些特殊情况,在这些情况下 finally 块可能无法按预期执行。

一种情况是,在 finally 块有机会执行之前,Java 虚拟机(JVM)异常退出。例如,如果我们在 try 块或捕获异常的 catch 块中调用了 System.exit () 方法,JVM 会立即终止程序,此时 finally 块将不会被执行。

另一种情况是,在 finally 块能够执行之前,运行 try-catch-finally 语句的线程被中断或终止。例如,如果我们使用 Thread.stop () 方法(尽管该方法已被弃用)来停止线程,或者由于一些外部因素(如内存不足错误)导致线程终止,此时 finally 块可能不会被执行。

此外,在一些极其罕见的情况下,如果 JVM 本身出现严重错误,导致程序以意想不到的方式崩溃,finally 块也可能不会被执行。但这些情况非常罕见,在正常编程中通常不会遇到。

需要注意的是,在大多数正常编程场景中,finally 块会按预期执行,并且它是确保某些清理或终结操作(如关闭资源)总是得以执行的非常有用的机制。

写出冒泡排序的代码实现,并分析在什么情况下冒泡排序比较高效。

冒泡排序是一种简单的排序算法。以下是使用 Java 实现冒泡排序的代码示例:

public class BubbleSort {
    public static void bubbleSort(int[] arr) {
        int n = arr.length;
        for (int i = 0; i < n - 1; i++) {
            for (int j = 0; j < n - i - 1; j++) {
                if (arr[j] > arr[j + 1]) {
                    // 交换元素位置
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
    }

    public static void main(String[] args) {
        int[] arr = {64, 34, 25, 12, 22, 11, 90};
        bubbleSort(arr);
        for (int num : arr) {
            System.out.print(num + " ");
        }
    }
}

冒泡排序在数据基本有序的情况下比较高效 。当数组已经接近有序时,冒泡排序的交换次数会大大减少。例如,对于一个只有少数元素位置不对的数组,在第一轮比较中,可能只需要进行很少的交换操作就能将数组排好序。而且冒泡排序的实现简单,易于理解和编写代码。它的时间复杂度在最好情况下是,即数组已经有序时,只需要进行一轮遍历比较,不需要进行交换操作。但在最坏情况下,时间复杂度为,例如数组是完全逆序的情况,需要进行多次遍历和交换。平均时间复杂度也是,空间复杂度为,因为只需要常数级的额外空间来进行元素的交换。

阐述快速排序的原理。

快速排序是一种高效的排序算法,它基于分治的思想。其基本原理如下:

首先,从数组中选择一个基准值(通常选择数组的第一个元素、最后一个元素或中间元素等)。然后,通过一趟排序将数组分为两部分,使得左边部分的所有元素都小于等于基准值,右边部分的所有元素都大于等于基准值。这个过程称为划分操作。

划分操作的具体实现是通过两个指针,一个从数组的左端开始向右移动,一个从数组的右端开始向左移动。当左指针指向的元素小于等于基准值时,左指针向右移动;当右指针指向的元素大于等于基准值时,右指针向左移动。当左指针和右指针相遇时,将相遇位置的元素与基准值交换,这样就完成了一次划分。

然后,对划分后的左右两部分分别递归地进行快速排序,直到整个数组都有序为止。

快速排序的时间复杂度在平均情况下是O(nlogn),这是因为每次划分操作都能将数组大致分为两部分,递归的深度大约为logn层,每层的比较次数大约为n次。但在最坏情况下,时间复杂度会退化为O(n^2),例如数组已经有序或接近有序时,每次划分都只能将数组分为一个元素和其余元素两部分。快速排序的空间复杂度在平均情况下是O(logn),最坏情况下是O(n),这取决于递归调用栈的深度。它是一种不稳定的排序算法,因为在划分过程中可能会改变相等元素的相对顺序。

在有多层循环嵌套的情况下,如何跳出所有循环?

在 Java 中,当有多层循环嵌套时,如果想要跳出所有循环,可以使用标签结合 break 语句来实现。以下是一个示例代码:

public class BreakLabelExample {
    public static void main(String[] args) {
        outerLoop:
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                if (i == 1 && j == 1) {
                    break outerLoop;
                }
                System.out.println("i = " + i + ", j = " + j);
            }
        }
    }
}

在上述代码中,我们给外层循环添加了一个标签 outerLoop 。当内层循环满足一定条件 i == 1 && j == 1 时,使用 break outerLoop 语句,这样就可以直接跳出外层循环,而不仅仅是内层循环。通过这种方式,我们可以在多层循环嵌套的复杂逻辑中,根据特定的条件一次性跳出所有不需要继续执行的循环,从而提高程序的效率和控制逻辑的准确性。

列举你所知道的网络协议。

网络协议是计算机网络中进行数据交换而建立的规则、标准或约定的集合。以下是一些常见的网络协议:

  • TCP/IP 协议簇:这是互联网的基础协议簇。其中,TCP(传输控制协议)是一种面向连接的、可靠的传输层协议。它通过三次握手建立连接,在数据传输过程中提供确认、重传等机制,保证数据的可靠传输。常用于如网页浏览(HTTP 基于 TCP)、文件传输(FTP 基于 TCP)等对可靠性要求较高的应用。IP(网际协议)是网络层协议,负责将数据包从源地址传输到目标地址,实现网络互联。
  • UDP(用户数据报协议):它是一种无连接的、不可靠的传输层协议。UDP 不保证数据的可靠传输,没有连接建立和拆除的过程,数据传输效率较高。常用于实时性要求较高但对可靠性要求相对较低的应用,如视频会议、实时游戏等。
  • HTTP(超文本传输协议):它是用于在网络上传输超文本的应用层协议,主要用于浏览器和服务器之间的数据交互,使得我们能够在浏览器中访问网页。它基于 TCP 协议,规定了客户端和服务器之间请求和响应的格式和交互方式。
  • HTTPS(超文本传输安全协议):它是 HTTP 的安全版本,在 HTTP 的基础上加入了 SSL/TLS 加密层,对数据进行加密传输,提高了数据的安全性,常用于网上银行、电子商务等对安全性要求较高的网站。
  • FTP(文件传输协议):用于在网络上进行文件传输的应用层协议,它基于 TCP 协议,支持文件的上传和下载,以及对文件和目录的管理操作。
  • SMTP(简单邮件传输协议):用于发送电子邮件的应用层协议,它规定了邮件的发送者、接收者、邮件内容等格式和传输方式,将邮件从发件人的邮件服务器传输到收件人的邮件服务器。
  • POP3(邮局协议第 3 版)和 IMAP(互联网消息访问协议):这两个协议用于接收电子邮件,POP3 通常将邮件从服务器下载到本地客户端后删除服务器上的邮件副本,而 IMAP 则允许用户在服务器上管理邮件,客户端可以与服务器上的邮件进行交互,如查看、移动、删除等操作,而不一定要下载到本地。

说出 HTTP 的默认端口。

HTTP 的默认端口是 80 。当我们在浏览器中输入一个网址时,如果没有指定端口号,浏览器会默认使用 80 端口与服务器进行连接,以获取网页等资源。例如,当我们访问 www.example.com 时,实际上浏览器是向服务器的 80 端口发送 HTTP 请求,服务器在 80 端口监听并接收请求,然后根据请求的内容返回相应的响应数据,如网页的 HTML 代码等。如果服务器上的 HTTP 服务配置了其他端口,那么在访问时就需要在网址中明确指定该端口号,如 www.example.com:8080 ,这里的 8080 就是指定的非默认端口。

说明你对 FTP 的了解情况。

FTP 即文件传输协议(File Transfer Protocol),是用于在网络上进行文件传输的标准协议,在互联网发展历程中有着重要地位。

从功能特性来讲,FTP 支持在客户端和服务器之间实现文件的上传(将本地文件传输到服务器端)和下载(从服务器端获取文件到本地)操作。它还能对服务器上的文件和目录进行管理,比如创建、删除、重命名目录,移动、复制、删除文件等操作。

FTP 基于 TCP 协议来确保数据传输的可靠性,在传输过程中会建立控制连接和数据连接。控制连接主要用于传输客户端和服务器之间的命令和响应,比如客户端发送获取文件列表的命令,服务器通过控制连接返回相应的文件列表信息。数据连接则专门用于实际的文件数据传输,当有文件上传或下载需求时,数据就通过这个连接进行传输。

从使用场景来看,在早期互联网应用不那么丰富的时候,FTP 广泛应用于网站维护人员向服务器上传网页文件、更新网站内容等场景。即使在现在,对于一些需要在不同设备或服务器之间频繁传输大量文件的场景,FTP 依然是一种可选的方案,比如企业内部在不同部门的服务器之间传输资料文件、开发团队向测试服务器上传测试版本的应用程序等。

从用户认证角度,FTP 支持多种认证方式,常见的有匿名登录和需要用户名密码的授权登录。匿名登录一般允许用户以特定的匿名账号(如 “anonymous”)访问服务器上指定的公共资源区域,这种方式适用于一些公开分享资源的服务器。而授权登录则需要用户提供合法的用户名和密码,才能访问服务器上对应的资源,通常用于企业内部、个人专属等对资源安全性有要求的情况。

阐述 HashMap 和 HashTable 的区别,包括是否用过以及它们在使用场景、特性等方面的不同。

一、是否用过

在实际开发中,这两种数据结构都经常会用到。例如在处理一些需要根据特定键快速查找对应值的情况时,可能会首先考虑 HashMap 或 HashTable。比如在一个用户信息管理系统中,要根据用户的唯一标识(如用户名或用户 ID)快速获取用户的详细信息,就可以使用它们来存储用户标识和用户信息的映射关系。

二、使用场景

  • HashMap:适用于单线程环境下,对性能要求较高,且不需要考虑线程同步问题的场景。比如在一个简单的单机应用程序中,用于存储一些配置信息,以键值对的形式,方便快速根据配置项的名称(键)获取配置的值。例如,存储应用程序的主题颜色设置、字体大小设置等信息,通过配置项名称就能快速找到对应的设置值。
  • HashTable:主要用于多线程环境,需要保证数据在并发访问时的一致性和线程安全性的情况。比如在一个多用户同时访问的服务器端应用程序中,用来存储在线用户的状态信息,不同线程(可能对应不同用户的操作)可能会频繁地查询、修改这些状态信息,HashTable 能确保在多线程操作下数据不会出现混乱。

三、特性

  • 线程安全性

    • HashMap:不是线程安全的。在多线程环境下,如果多个线程同时对 HashMap 进行读写操作,很可能会导致数据不一致的问题。例如,两个线程同时对同一个键进行 put 操作,可能会出现数据覆盖或者其他不可预期的情况。
    • HashTable:是线程安全的。它内部的方法基本都被 synchronized 关键字修饰,这使得在多线程访问时,同一时刻只有一个线程能对 HashTable 进行操作,从而保证了数据的一致性和完整性。但这种线程安全的实现方式也带来了一定的性能损耗,因为加锁会导致其他线程需要等待锁的释放才能进行操作。
  • 键值对允许情况

    • HashMap:允许键和值都为 null。可以使用 null 作为键来存储一个特定的值,也可以让值为 null 来表示某种特殊的状态或者未初始化的情况。例如,在存储一些可选的配置项时,如果某个配置项没有设置具体的值,就可以用 null 来表示。
    • HashTable:不允许键或值为 null。如果尝试将 null 作为键或值放入 HashTable 中,会抛出 NullPointerException 异常。这是因为 HashTable 在设计时就考虑到了要保证数据的严谨性和一致性,不允许出现这种可能导致歧义的 null 值情况。
  • 迭代器的一致性

    • HashMap:在迭代过程中,如果对 HashMap 进行了结构上的修改(如添加、删除元素),迭代器可能会抛出 ConcurrentModificationException 异常。这是因为 HashMap 的迭代器在设计时是基于快速失败机制的,一旦检测到集合结构发生了改变,就会提示错误。
    • HashTable:同样在迭代过程中,如果对 HashTable 进行了结构上的修改,也会抛出 ConcurrentModificationException 异常。不过,由于 HashTable 本身是线程安全的,在多线程环境下这种情况相对较少出现,因为它的操作都是串行化的,一般不会出现多个线程同时修改导致迭代器异常的情况。

比较 List、Map、Set 存储的元素有哪些不同,包括是否有序、元素类型(是否键值对)等方面。

一、是否有序

  • List:是有序的集合,它维护了元素插入的顺序。也就是说,按照元素添加到 List 中的先后顺序,在遍历 List 时会依次输出这些元素。例如,创建一个 ArrayList 并依次添加元素 “apple”、“banana”、“cherry”,那么在遍历这个 ArrayList 时,就会按照添加的顺序依次输出这三个元素。

  • Map:本身并不是按照传统意义上的顺序来存储元素的,它主要是通过键值对的形式来存储数据,重点在于通过键能快速找到对应的 值。不过,从 Java 8 开始,有些 Map 的实现类(如 LinkedHashMap)在一定程度上可以保持元素的插入顺序或者访问顺序,但这并不是 Map 这个接口本身所强调的特性。

  • Set:一般情况下是无序的集合,它只关注元素的唯一性,不关心元素的插入顺序。例如,创建一个 HashSet 并依次添加元素 “apple”、“banana”、“cherry”,在遍历 HashSet 时,输出元素的顺序可能是不确定的,每次遍历可能得到不同的顺序,因为它主要目的是确保集合中没有重复的元素。

二、元素类型(是否键值对)

  • List:存储的是单个元素,不是键值对的形式。例如,可以在 List 中存储整数、字符串、对象等各种类型的单个元素。比如,创建一个 List 来存储学生的成绩,就可以直接将每个学生的成绩作为一个单独的元素添加到 List 中。

  • Map:存储的是键值对形式的元素。键是用来唯一标识一个值的,通过键可以快速找到对应的 值。键和值可以是不同类型的对象,比如可以用字符串作为键来存储对应的整数 值,如用 “studentName” 作为键,存储对应的学生成绩作为值。

  • Set:存储的也是单个元素,和 List 类似,但它强调元素的唯一性。例如,在一个 HashSet 中存储不同的用户名,每个用户名就是一个单独的元素,它不会出现两个相同的用户名同时存在于这个集合中的情况。

说明 List、Map、Set 是否可以有重复元素。

一、List

List 是可以有重复元素的集合类型。它主要关注元素的顺序,允许在同一个 List 中添加多个相同的元素。例如,创建一个 ArrayList 并添加元素 “apple”、“banana”、“apple”,这个 ArrayList 中就包含了两个 “apple” 元素。这是因为 List 的设计初衷是为了能够按照顺序存储一系列可能存在重复情况的元素,比如在存储学生的考试成绩时,可能会有多个学生得到相同的分数,就可以直接将这些分数依次添加到 List 中。

二、Map

Map 从严格意义上来说,不存在重复元素的概念,因为它存储的是键值对,并且键是必须唯一的。通过键来唯一确定一个值,如果尝试向 Map 中添加一个已经存在的键的键值对,通常会覆盖原来的键值对。例如,在一个 HashMap 中,如果已经有一个键为 “studentName” 的键值对,当再次添加一个键为 ““studentName” 的键值对时,新的值会替换掉原来的值。不过,从值的角度来看,Map 可以有相同的值对应不同的键,比如可以用 “student1” 作为键,“80” 作为值,同时也可以用 “student2” 作为键,“80” 作为值。

三、Set

Set 是不允许有重复元素的集合类型。它的主要目的就是确保集合中元素的唯一性。例如,创建一个 HashSet 并添加元素 “apple”、“banana”、“apple”,实际上最终在 HashSet 中只会存在 “apple” 和 “banana” 这两个元素,因为它会自动排除掉重复的元素。这使得 Set 在很多需要确保元素唯一性的场景中非常有用,比如在存储用户的用户名时,不希望出现两个相同的用户名,就可以使用 Set 来存储。

对于一个有 10 个元素的列表,在循环删除元素时,如何保证不会产生索引越界的异常?写出具体实现方法。

当在循环中删除列表中的元素时,如果不注意处理方式,很容易产生索引越界的异常。以下是一种可以保证不会产生索引越界异常的具体实现方法:

我们可以使用倒序循环的方式来删除元素。以 Java 为例,假设我们有一个 ArrayList 类型的列表 list,里面有 10 个元素。

import java.util.ArrayList;

public class ListElementDeletion {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        // 添加10个元素到列表
        for (int i = 0; i < 10; i++) {
            list.add(i);
        }

        // 使用倒序循环删除元素
        for (int i = list.size() - 1; i >= 0; i--) {
            // 这里可以根据具体需求判断是否删除该元素
            if (需要删除的条件) {
                list.remove(i);
            }
        }

        // 可以在这里继续对处理后的列表进行其他操作
    }
}

在上述代码中,我们首先创建了一个包含 10 个元素的 ArrayList。然后,我们采用倒序循环的方式,从列表的最后一个元素开始,逐步向前处理。在循环体中,我们可以根据具体的需求来判断是否要删除当前索引对应的元素。如果满足删除条件,就使用 list.remove (i) 来删除该元素。

之所以采用倒序循环,是因为当我们从列表的末尾开始删除元素时,后面的元素索引不会受到前面删除操作的影响。如果采用正序循环,当我们删除一个元素后,后面元素的索引会依次往前移动,这样就很容易导致在后续的循环中出现索引越界的情况。例如,在正序循环中,如果我们删除了索引为 3 的元素,那么原来索引为 4 的元素就会变成索引为 3 的元素,而如果我们下一次循环的判断条件是基于原来的索引体系,就可能会出现索引越界的错误。而倒序循环就可以有效避免这种情况,保证在循环删除元素的过程中不会产生索引越界的异常。

详细说明单例模式,并且列举你还学过的其他设计模式,比如工厂模式等。

单例模式是一种创建型设计模式,其核心在于确保一个类只有一个实例,并提供一个全局访问点来访问该实例 。它的主要目的是为了节约系统资源,当一个对象的创建和销毁成本较高,或者多个地方都需要使用同一个对象时,单例模式就非常有用。

单例模式有多种实现方式,常见的有饿汉式和懒汉式。饿汉式在类加载时就创建实例,优点是实现简单,线程安全,缺点是如果实例占用资源较多且不一定会被使用,会造成资源浪费。懒汉式则是在第一次使用时才创建实例,优点是节省资源,缺点是在多线程环境下需要考虑线程安全问题。

除了单例模式,还有很多其他设计模式。例如工厂模式,它提供了一种创建对象的方式,将对象的创建和使用分离。工厂模式又分为简单工厂模式、工厂方法模式和抽象工厂模式。简单工厂模式通过一个工厂类来负责创建对象,不符合开闭原则;工厂方法模式将工厂类的创建方法抽象成抽象方法,由具体子类实现,符合开闭原则;抽象工厂模式则是围绕一个超级工厂创建其他工厂,这些工厂再负责创建不同类型的对象。

还有观察者模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象,当主题对象状态发生变化时,会通知所有观察者对象。适用于当一个对象的状态变化需要通知其他多个对象的场景。

再如策略模式,它定义了一系列算法,将每个算法封装起来,并使它们可以相互替换。使得算法可以独立于使用它的客户而变化。常用于有多种不同算法实现同一功能,且需要根据不同情况动态切换算法的场景 。

阐述 Java 的三大特性。并说明在什么情况下需要进行继承。

Java 的三大特性分别是封装、继承和多态。

封装是指将对象的状态信息隐藏在对象内部,不允许外部程序直接访问,而是通过该类提供的方法来实现对内部信息的操作和访问。这样做的好处是提高了代码的安全性和可维护性,隐藏了类的实现细节,只对外提供必要的接口,使得代码的耦合度降低。例如,一个银行账户类,账户的余额等信息被封装在类内部,外部只能通过存款、取款等方法来间接操作余额,而不能直接修改余额的值。

继承是一种类与类之间的关系,它允许创建的新类继承现有类的属性和方法,新类被称为子类,现有类被称为父类。通过继承,子类可以复用父类的代码,减少代码的重复性。同时,子类还可以根据自身的需求扩展或修改父类的行为。例如,在一个图形绘制系统中,有圆形、矩形等各种图形类,它们都具有一些共同的属性和方法,如颜色、坐标等,可以先创建一个图形基类,将这些共同的属性和方法放在基类中,然后让圆形、矩形等类继承图形基类,各自再添加自己独特的属性和方法,如圆形的半径,矩形的长和宽等。

多态是指同一个行为具有多个不同表现形式或形态的能力。在 Java 中,多态主要通过方法重写和方法重载来实现。方法重写是指在子类中重新定义父类中已有的方法,方法重载是指在同一个类中定义多个同名方法,但参数列表不同。多态使得程序更加灵活和可扩展,提高了代码的复用性和可维护性。例如,在一个动物叫声模拟系统中,有动物类,以及它的子类狗和猫,动物类中有叫的方法,狗和猫类分别重写了这个叫的方法,实现各自不同的叫声,当通过动物类的引用调用叫的方法时,会根据实际指向的对象是狗还是猫而执行不同的叫声方法,这就是多态的体现。

在以下几种情况下需要进行继承:一是当有多个类存在共同的属性和行为时,可以将这些共同的部分提取到父类中,让子类继承,以减少代码的重复。二是当需要建立类之间的层次关系,以更好地组织和管理代码时,例如在一个公司的员工管理系统中,有普通员工、经理等不同类型的员工,他们有一些共同的属性和方法,同时又有各自不同的特性,可以创建员工类作为父类,普通员工和经理类作为子类进行继承。三是当需要实现多态时,继承是多态的基础,通过继承可以在子类中重写父类的方法,从而实现不同的行为表现。

写出单例模式中的双重校验锁实现方式以及静态内部类实现方式。

双重校验锁实现方式

public class Singleton {
    // 声明一个volatile修饰的静态变量,用于存储单例实例
    private volatile static Singleton instance;

    // 私有构造函数,防止外部通过构造函数创建实例
    private Singleton() {}

    // 提供一个公共的静态方法获取单例实例
    public static Singleton getInstance() {
        if (instance == null) {
            // 第一次检查,如果实例为空,则进入同步块
            synchronized (Singleton.class) {
                if (instance == null) {
                    // 第二次检查,在同步块内再次检查实例是否为空,如果为空则创建实例
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

在双重校验锁实现单例模式中,首先使用了 volatile 关键字修饰实例变量,这是为了防止指令重排序。因为在创建实例时,可能会发生指令重排序,导致在对象还未完全初始化时就被其他线程获取到,从而引发错误。然后通过两次检查实例是否为空,第一次检查是为了避免不必要的同步开销,如果实例已经存在,就直接返回。第二次检查是在同步块内,确保在多线程环境下只有一个线程能够创建实例。

静态内部类实现方式

public class Singleton {

    // 私有构造函数,防止外部通过构造函数创建实例
    private Singleton() {}

    // 定义一个静态内部类,在内部类中创建单例实例
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    // 提供一个公共的静态方法获取单例实例
    public static Singleton getInstance() {
        // 通过调用静态内部类的静态变量来获取单例实例
        return SingletonHolder.INSTANCE;
    }
}

静态内部类实现单例模式的原理是利用了类加载机制的特性。当外部类被加载时,内部类不会被立即加载,只有在第一次调用 getInstance 方法时,才会加载静态内部类 SingletonHolder,从而创建单例实例 INSTANCE。由于类加载过程是线程安全的,所以这种方式实现的单例模式也是线程安全的,同时还避免了使用同步锁带来的性能开销。

写出观察者模式的代码实现。

以下是一个简单的观察者模式的代码实现示例:

import java.util.ArrayList;
import java.util.List;

// 定义观察者接口
interface Observer {
    void update(String message);
}

// 定义被观察的主题接口
interface Subject {
    void registerObserver(Observer observer);

    void removeObserver(Observer observer);

    void notifyObservers(String message);
}

// 具体的主题类
class ConcreteSubject implements Subject {
    private List<Observer> observers = new ArrayList<>();

    @Override
    public void registerObserver(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    @Override
    public void notifyObservers(String message) {
        for (Observer observer : observers) {
            observer.update(message);
        }
    }
}

// 具体的观察者类A
class ConcreteObserverA implements Observer {
    @Override
    public void update(String message) {
        System.out.println("ConcreteObserverA received message: " + message);
    }
}

// 具体的观察者类B
class ConcreteObserverB implements Observer {
    @Override
    public void update(String message) {
        System.out.println("ConcreteObserverB received message: " + message);
    }
}

// 测试类
public class ObserverPatternTest {
    public static void main(String[] args) {
        ConcreteSubject subject = new ConcreteSubject();

        ConcreteObserverA observerA = new ConcreteObserverA();
        ConcreteObserverB observerB = new ConcreteObserverB();

        subject.registerObserver(observerA);
        subject.registerObserver(observerB);

        subject.notifyObservers("Hello, observers!");

        subject.removeObserver(observerA);

        subject.notifyObservers("Observer A has been removed.");
    }
}

在这个示例中,首先定义了 Observer 接口,它有一个 update 方法,用于接收被观察主题的状态变化通知并进行相应的处理。然后定义了 Subject 接口,它包含了注册观察者、移除观察者和通知观察者的方法。

ConcreteSubject 类是具体的被观察主题类,它维护了一个观察者列表,实现了 Subject 接口的方法,当状态发生变化时,通过遍历观察者列表并调用每个观察者的 update 方法来通知所有观察者。

ConcreteObserverA 和 ConcreteObserverB 是具体的观察者类,它们实现了 Observer 接口的 update 方法,在接收到通知时进行相应的操作,这里只是简单地打印出收到的消息。

在 ObserverPatternTest 类的 main 方法中,创建了被观察主题对象和两个观察者对象,然后注册观察者,发送通知,移除观察者并再次发送通知,演示了观察者模式的基本用法。

列举 Android 的四大组件,并分别简述。

Activity(活动)

Activity 是 Android 中最基本的组件,用于实现用户界面。它提供了一个可视化的区域,用户可以在其中与应用进行交互,如点击按钮、输入文本等。每个 Activity 通常对应一个屏幕的内容,它可以包含各种视图控件,如 TextView、Button 等。Activity 之间可以通过 Intent 进行切换和传递数据,形成一个完整的用户界面流程。例如,一个登录界面可以是一个 Activity,用户在其中输入用户名和密码后,点击登录按钮可以跳转到主界面 Activity,同时可以将用户名等信息传递过去。

Service(服务)

Service 是一种在后台执行长时间运行操作而不提供用户界面的组件。它用于执行一些不需要与用户直接交互的任务,如音乐播放、文件下载、网络数据获取等。Service 可以在应用的整个生命周期内持续运行,即使应用的 Activity 被切换或关闭。它有两种启动方式,一种是通过 startService 方法启动,这种方式启动的 Service 会一直运行直到被停止;另一种是通过 bindService 方法启动,这种方式启动的 Service 与启动它的组件绑定在一起,当所有绑定的组件都解除绑定后,Service 才会停止。

Broadcast Receiver(广播接收器)

Broadcast Receiver 用于接收系统或应用发出的广播消息。广播是一种广泛传播的消息机制,系统会在某些特定的事件发生时发送广播,如电池电量变化、网络连接变化、开机完成等,应用也可以自定义广播并发送。Broadcast Receiver 可以注册为静态或动态的,静态注册是在 AndroidManifest.xml 文件中声明,应用安装后就会一直监听广播;动态注册是在代码中通过 registerReceiver 方法注册,通常在 Activity 或 Service 的生命周期内进行注册和注销。当接收到广播时,Broadcast Receiver 会执行相应的 onReceive 方法来处理广播事件。

Content Provider(内容提供者)

Content Provider 用于在不同的应用之间共享数据。它提供了一种统一的接口,使得应用可以安全地访问和操作其他应用的数据,同时也可以将自己的数据暴露给其他应用。Content Provider 通常用于访问系统的一些数据,如联系人、短信、多媒体文件等,也可以用于应用内部不同模块之间的数据共享。它通过 ContentResolver 来进行数据的访问和操作,应用可以使用 ContentResolver 的方法来插入、查询、更新和删除 Content Provider 提供的数据。

阐述 Android 中 view 的绘制流程

Android 中 View 的绘制流程主要分为三个阶段:测量、布局和绘制 。

测量阶段:从根视图开始,递归地测量每个子视图的大小。父视图会根据自身的布局参数和子视图的要求来确定子视图的测量大小。此阶段主要是通过 measure 方法来完成,它会调用 onMeasure 方法,在 onMeasure 中,视图需要根据自身的需求和父视图传递过来的约束条件来计算出自己的测量宽高。例如,一个 TextView 会根据其文本内容、字体大小等因素来确定自己的测量宽度和高度。

布局阶段:在测量阶段完成后,开始进行布局。同样从根视图开始,递归地确定每个子视图的位置。父视图会根据自身的布局方式和子视图的测量大小来安排子视图的位置。通过 layout 方法来实现,它会调用 onLayout 方法,对于不同的布局容器,如 LinearLayout、RelativeLayout 等,其 onLayout 的实现方式不同。LinearLayout 会按照水平或垂直方向依次排列子视图,而 RelativeLayout 则会根据子视图之间的相对位置关系来布局。

绘制阶段:在布局确定后,开始进行绘制。此阶段会将视图的内容绘制到屏幕上,通过 draw 方法来完成,它会调用 onDraw 方法。在 onDraw 中,视图可以使用 Canvas 对象来进行图形绘制,如绘制线条、矩形、圆形等,还可以绘制文本等内容。例如,自定义一个 View 时,可以在 onDraw 中根据业务需求绘制出特定的图形或界面效果。

解释 Android 中 handler 的消息传递机制

Handler 主要用于在不同线程之间进行消息传递和处理。

在 Android 中,主线程负责更新 UI,如果在子线程中直接更新 UI 会导致程序出错。Handler 就提供了一种机制,让子线程可以通过发送消息给主线程来间接更新 UI 。

消息发送:子线程可以通过创建一个 Handler 对象,然后使用该对象的 sendMessage 方法来发送消息。消息可以携带一些数据,例如一个整数、字符串或自定义的对象等。发送的消息会被存储在与该 Handler 关联的消息队列中。

消息循环与处理:主线程中有一个消息循环,它会不断地从消息队列中取出消息进行处理。当有消息到达时,消息循环会根据消息的目标 Handler 将消息分发给对应的 Handler 进行处理。

处理消息:Handler 会通过重写 handleMessage 方法来处理接收到的消息。在 handleMessage 方法中,可以根据消息的内容进行相应的操作,比如更新 UI 界面上的某个控件的文本、颜色等属性。

列举导致 Android 内存泄漏的原因

资源未正确关闭:例如在使用文件流、数据库连接、网络连接等资源后,没有及时关闭。这些资源会一直占用内存,导致内存泄漏。比如在使用 SQLite 数据库操作后,如果没有关闭数据库连接,那么这个连接所占用的内存就无法释放。

注册未正确注销:一些需要注册的对象,如广播接收器、事件监听器等,如果在使用后没有正确注销,会导致内存泄漏。以广播接收器为例,如果在 Activity 中注册了一个全局广播接收器,当 Activity 销毁时没有注销该接收器,那么即使 Activity 已经不存在了,该接收器仍然会接收广播并占用内存。

持有不必要的引用:当一个对象被长期持有,而它又引用了其他应该被释放的对象时,就会导致内存泄漏。比如在一个 Activity 中,定义了一个静态的变量引用了该 Activity 本身,那么即使 Activity 应该被销毁,但由于被静态变量引用,无法被释放。

容器类对象无限增长:在使用一些集合类对象时,如果不断地向其中添加元素而没有合理的删除机制,会导致集合无限增长,占用大量内存。例如在一个列表中,不断地添加数据而没有删除不需要的数据,最终会导致内存泄漏。

如果一个视图可以用 LinearLayout 和 RelativeLayout 都能实现,分析应该选择哪一个,并说明原因

性能方面

  • LinearLayout:在简单的线性布局场景下,性能较好。它的布局计算相对简单,只是按照水平或垂直方向依次排列子视图,不需要复杂的相对位置计算。例如,对于一个简单的列表布局,LinearLayout 可以快速地完成布局计算和绘制。
  • RelativeLayout:相对布局的性能在复杂布局时可能会稍差一些,因为它需要计算子视图之间的相对位置关系,这个计算过程相对复杂。尤其是当布局中存在大量的相对位置约束时,性能消耗会更明显。

布局灵活性

  • LinearLayout:适合简单的线性排列布局,如水平或垂直方向的列表。但如果需要实现复杂的相对位置布局,就会比较困难。例如,要实现一个子视图在另一个子视图的下方且居中对齐,使用 LinearLayout 就不太容易实现。
  • RelativeLayout:布局非常灵活,可以轻松实现各种复杂的相对位置布局。比如可以方便地让一个视图位于另一个视图的左边、右边、上方、下方等任意位置,还可以设置相对偏移量等。

代码可读性和维护性

  • LinearLayout:代码结构相对简单,对于简单布局,代码易于理解和维护。开发者可以很直观地看到子视图的排列顺序和方向。
  • RelativeLayout:在布局复杂时,虽然功能强大,但代码可能会变得比较复杂,难以阅读和维护。过多的相对位置约束会使布局文件看起来混乱,增加了开发和维护的难度。

解释 invalidate () 和 postInvalidate () 的区别以及它们各自的使用场景

区别

  • invalidate():是 View 类的方法,用于请求重绘视图。它会使视图的整个区域无效,然后触发视图的 onDraw 方法进行重绘。调用 invalidate () 方法后,会立即返回,重绘操作是异步进行的,由 Android 系统的 UI 线程在合适的时机进行处理。
  • postInvalidate():也是用于请求重绘视图,但它是在非 UI 线程中调用的方法。它会将重绘请求发送到 UI 线程的消息队列中,等待 UI 线程处理。与 invalidate () 不同的是,它可以在非 UI 线程中安全地调用,避免了在非 UI 线程中直接调用 invalidate () 可能导致的线程安全问题。

使用场景

  • invalidate():通常在 UI 线程中,当视图的内容发生变化需要重绘时使用。例如,在一个自定义 View 中,当用户操作导致视图的显示内容需要更新时,如改变了图形的颜色、形状等,可以在 UI 线程中调用 invalidate () 来触发重绘。
  • postInvalidate():主要用于在非 UI 线程中需要更新 UI 的情况。比如在一个后台线程中,完成了一些数据的加载或处理,需要根据新的数据更新界面显示,就可以调用 postInvalidate () 来请求重绘视图,它会将重绘请求发送到 UI 线程,由 UI 线程来执行重绘操作,确保了线程安全。

说明 http 的包头包含哪些内容

HTTP 包头是在 HTTP 请求和响应消息中,位于起始行之后,用于传递额外的元信息,它包含了许多重要的字段:

  • 通用首部字段:这些字段适用于请求和响应消息。例如,“Date” 字段表示消息发送的日期和时间,遵循特定的日期格式,它有助于客户端和服务器了解消息的时间顺序,对于缓存和日志记录等功能很重要;“Cache-Control” 字段用于指定缓存机制,如是否允许缓存、缓存的有效期等,通过合理设置可以提高网络性能和资源利用率。
  • 请求首部字段:在 HTTP 请求中使用。“User-Agent” 字段包含了客户端的相关信息,如浏览器类型、版本号等,服务器可以根据这个字段来针对不同的客户端提供不同的内容或进行统计分析;“Referer” 字段用于指定当前请求的来源页面,在防盗链、统计分析等方面有重要作用。
  • 响应首部字段:用于 HTTP 响应消息。“Server” 字段表明了服务器软件的名称和版本,有助于客户端了解服务器的特性和功能;“Set-Cookie” 字段用于在客户端设置 Cookie,Cookie 可用于存储用户的登录状态、浏览偏好等信息,方便后续的交互和个性化服务。
  • 实体首部字段:与消息主体相关。比如 “Content-Type” 字段用于指定消息主体的媒体类型,如 “text/html” 表示 HTML 文档,“image/jpeg” 表示 JPEG 图片等,客户端可以根据此类型来正确解析和处理数据;“Content-Length” 字段则表示消息主体的长度,以字节为单位,这对于准确接收和处理数据很关键。

阐述 https 的加密过程以及涉及的证书相关知识

HTTPS 即超文本传输安全协议,它的加密过程如下:

  • 协商加密算法和密钥交换:客户端向服务器发起 HTTPS 连接请求,服务器返回包含其公钥证书的信息。客户端和服务器在这个阶段协商要使用的加密算法,如 RSA、ECDHE 等。然后通过密钥交换算法,客户端和服务器共同生成一个用于本次会话的对称加密密钥。这个对称密钥将用于后续的数据加密和解密,对称加密算法相比非对称加密算法在数据传输时效率更高。
  • 验证服务器证书:客户端收到服务器的公钥证书后,会验证证书的有效性。客户端会检查证书是否由受信任的证书颁发机构(CA)颁发,证书是否在有效期内,证书的域名与服务器的实际域名是否匹配等。如果证书验证通过,客户端就可以信任服务器的身份,并使用服务器的公钥来加密后续的通信数据。
  • 数据加密传输:在协商好加密算法和验证服务器证书后,客户端和服务器就可以使用对称加密密钥对数据进行加密传输。客户端发送的数据使用对称密钥加密后发送给服务器,服务器收到加密数据后,使用相同的对称密钥进行解密。这样,即使数据在传输过程中被拦截,第三方也无法获取其中的内容。

关于证书相关知识:

  • 证书颁发机构(CA):是负责颁发数字证书的权威机构,它通过一系列严格的验证流程来确认服务器的身份信息,然后颁发证书。常见的 CA 有 Symantec、Comodo 等。这些 CA 的根证书通常已经预装在主流的操作系统和浏览器中,所以客户端可以信任它们颁发的证书。
  • 证书内容:证书中包含了服务器的公钥、服务器的身份信息(如域名、公司名称等)、证书的有效期、CA 的数字签名等。CA 的数字签名是通过 CA 的私钥对证书的摘要进行加密生成的,客户端可以使用 CA 的公钥来验证这个签名,从而确认证书的真实性和完整性。

描述 Android 自定义 view 时滑动冲突的产生原因以及解决办法

  • 产生原因
    • 外部滑动方向与内部滑动方向不一致:例如,在一个水平方向可滑动的 ViewGroup 中嵌套了一个垂直方向可滑动的 ListView。当用户在 ListView 上进行垂直滑动操作时,外部的 ViewGroup 可能会拦截并处理该滑动事件,导致 ListView 的滑动不流畅,这就产生了滑动冲突。
    • 外部滑动方向与内部滑动方向一致:常见于多个可滑动的 View 嵌套的情况,如在一个垂直方向可滑动的 ScrollView 中嵌套了另一个垂直方向可滑动的 LinearLayout。在这种情况下,滑动事件可能会被错误地分配,导致滑动行为不符合预期。
    • 多层嵌套的复杂布局:当布局结构较为复杂,存在多层嵌套的可滑动 View 时,滑动事件的传递和处理机制会变得复杂,容易出现滑动冲突。不同层级的 View 可能会对滑动事件有不同的处理需求,从而导致冲突的发生。
  • 解决办法
    • 外部拦截法:父容器根据自身的逻辑来决定是否拦截滑动事件。通过重写父容器的 onInterceptTouchEvent 方法,在该方法中根据滑动的方向、距离等条件来判断是否拦截事件。例如,如果水平方向的滑动距离超过一定阈值,父容器就拦截事件进行水平滑动处理;否则,将事件传递给子 View 进行处理。
    • 内部拦截法:子 View 根据自身的需求来决定是否拦截滑动事件。子 View 重写 dispatchTouchEvent 方法,在方法中通过 getParent ().requestDisallowInterceptTouchEvent 方法来告诉父容器是否要拦截事件。当子 View 需要处理滑动事件时,调用该方法阻止父容器拦截事件;当子 View 不需要处理时,允许父容器拦截事件。
    • 使用事件分发的辅助类:Android 提供了一些辅助类来帮助处理滑动冲突,如 ViewPager 和 NestedScrollView。ViewPager 可以很好地处理页面切换的滑动操作,而 NestedScrollView 则支持在嵌套的 ScrollView 中正确地处理滑动事件。在合适的场景下使用这些辅助类,可以有效地避免手动处理滑动冲突带来的复杂性。

解释 Android 程序是如何编译成一个 apk 的过程

  • 编写代码:开发人员使用 Java 或 Kotlin 等编程语言编写 Android 应用程序的源代码。这些代码包括各种 Android 组件,如 Activity、Service、Broadcast Receiver 和 Content Provider 等,以及相关的业务逻辑和界面布局代码。
  • 编译代码:使用 Android 开发工具包(SDK)中的编译器,将编写好的源代码编译成字节码文件。对于 Java 代码,编译器会将其编译成.class 文件;对于 Kotlin 代码,编译器会将其编译成与 Java 字节码兼容的字节码文件。
  • 资源处理:Android 应用程序通常包含各种资源,如布局文件、图片、字符串等。编译器会对这些资源进行处理,将它们转换为可以被 Android 系统识别和使用的格式,并为每个资源生成一个唯一的标识符,以便在代码中引用。
  • 生成.dex 文件:将编译后的字节码文件和依赖的库文件通过 dx 工具转换为 Dalvik 字节码文件,即.dex 文件。Dalvik 是 Android 系统早期使用的虚拟机,它可以高效地运行在移动设备上。虽然现在 Android 已经转向使用 ART 虚拟机,但仍然需要将字节码转换为.dex 格式。
  • 打包成 APK:将.dex 文件、处理后的资源文件以及 AndroidManifest.xml 文件等打包成一个 APK 文件。APK 文件是 Android 应用程序的安装包格式,它实际上是一个 ZIP 压缩文件,包含了应用程序的所有代码、资源和配置信息。
  • 签名:为了保证应用程序的完整性和安全性,APK 文件需要进行签名。开发人员使用私钥对 APK 文件进行签名,签名后的 APK 文件在安装和更新时会被 Android 系统验证其签名的有效性。如果签名不匹配,系统将拒绝安装或更新应用程序。
  • 对齐优化:最后,对签名后的 APK 文件进行对齐优化操作。这一步可以使 APK 文件在设备上的存储和加载更加高效,提高应用程序的启动速度和性能。

列举 Activity launch mode 的应用场景

  • standard 模式
    • 频繁创建和销毁的临时界面:适用于那些不需要保存状态,频繁创建和销毁的界面,比如一些临时的提示框或简单的信息展示界面。例如,一个应用中的 “登录成功提示” 界面,用户登录成功后显示一个短暂的提示框告知用户登录成功,这个提示框不需要保存任何状态,使用完即销毁,下次再需要显示时重新创建即可,采用 standard 模式可以方便地实现这种频繁创建和销毁的需求。
    • 多实例需求:当需要在应用中同时存在多个相同 Activity 的实例时,standard 模式是合适的选择。比如一个音乐播放应用,用户在不同的播放列表中点击播放歌曲时,可能希望每个播放列表都能独立打开一个播放歌曲的 Activity 实例,以便方便地在不同列表之间切换播放,此时使用 standard 模式就能满足这种多实例的需求。
  • singleTop 模式
    • 避免重复创建相同界面:当一个 Activity 已经位于栈顶时,如果再次启动该 Activity,不需要重新创建新的实例,而是直接复用栈顶的实例。这在一些场景下可以避免重复创建相同的界面,提高性能和用户体验。例如,在一个新闻应用中,用户点击一篇新闻进入新闻详情页,如果用户在详情页中点击了某个相关链接又跳转到了同一个新闻详情页,使用 singleTop 模式就可以直接复用已有的详情页实例,而不是重新创建一个新的,这样可以保留用户在原详情页的浏览位置、滚动状态等信息。
    • 通知栏点击跳转:当通过点击通知栏消息启动一个 Activity 时,如果该 Activity 已经在栈顶,使用 singleTop 模式可以直接将通知栏的意图传递给已有的 Activity 实例进行处理,而不是创建新的实例,从而保证了操作的连贯性和一致性。
  • singleTask 模式
    • 主界面或核心功能界面:适用于应用的主界面或核心功能界面,保证整个应用中只有一个该 Activity 的实例存在。例如,一个社交应用的主页面,无论用户从应用的哪个地方跳转,最终返回主页面时都应该是同一个实例,这样可以保证主页面的状态和数据的一致性,避免出现多个主页面实例导致的数据混乱和资源浪费。
    • 多模块共享界面:在一个包含多个功能模块的应用中,如果有一些界面在不同模块中都可能被访问到,且希望这些界面在整个应用中只有一个实例,那么可以使用 singleTask 模式。比如,一个电商应用中,商品详情页可能在搜索模块、分类浏览模块等多个地方都能被访问到,使用 singleTask 模式可以保证无论从哪个模块进入商品详情页,都是同一个实例,方便用户操作和数据管理。
  • singleInstance 模式
    • 独立的全局界面:用于那些需要在整个系统中独立存在,与应用的其他 Activity 具有不同任务栈的界面。例如,一个应用中的视频通话界面,它可能需要在系统中独立运行,不与应用的其他界面共享任务栈,以便在视频通话过程中,用户可以方便地切换到其他应用进行其他操作,而视频通话界面仍然保持运行状态,不会受到其他应用的影响,此时使用 singleInstance 模式可以很好地满足这种需求。
    • 系统级功能界面:某些具有系统级功能的界面,如一些与设备硬件交互密切的设置界面,可能需要独立于应用的常规界面运行,以确保其稳定性和独立性。采用 singleInstance 模式可以将这些界面与应用的其他部分隔离开来,避免因应用的其他操作对其产生干扰。

说明 Service 在项目中的常见使用场景以及其作用

Service 是一种可在后台执行长时间运行操作而不提供用户界面的应用组件 。

  • 常见使用场景及作用之音乐播放:当开发音乐播放类应用时,用户可能在进行其他操作比如浏览网页、使用其他应用时,仍希望音乐持续播放。Service 就可以在后台持续运行音乐播放的逻辑,不依赖于任何 Activity 的界面显示。它能独立于应用的可见界面,在后台处理音乐的解码、播放、暂停、停止等操作,保证音乐播放的持续性和稳定性。
  • 常见使用场景及作用之文件下载:如果应用需要从网络上下载较大的文件,如视频、大型文档等,使用 Service 可以让下载过程在后台进行。这样用户可以继续与应用的其他部分进行交互,或者切换到其他应用,而不会因为下载操作阻塞当前界面,提高了用户体验和应用的可用性。
  • 常见使用场景及作用之数据同步:对于需要定期与服务器进行数据同步的应用,例如邮件客户端需要定时检查是否有新邮件,Service 可以在后台按照设定的时间间隔自动发起数据同步请求,获取最新的数据并更新本地存储,保证应用数据的及时性和准确性,且整个过程对用户是无感知的,不影响用户正常使用应用的其他功能 。

解释为什么在项目中采用 Service,而不是其他类似 Handler 等的方法

  • 生命周期角度:Service 有自己独立的生命周期,它可以在后台持续运行,不依赖于 Activity 的生命周期。比如当 Activity 被销毁后,Service 仍然可以继续工作。而 Handler 通常是与某个线程或 Activity 的生命周期绑定的,当 Activity 销毁时,Handler 如果处理不当,容易导致内存泄漏或任务中断等问题。例如在一个 Activity 中使用 Handler 进行网络请求,当 Activity 意外关闭时,Handler 可能还在等待网络响应,从而引发潜在的问题。
  • 执行长时间任务方面:Service 适合执行长时间的后台任务,如上述提到的音乐播放、文件下载等。它可以在后台持续运行数小时甚至数天,只要系统资源允许。而 Handler 主要是用于在主线程和子线程之间传递消息和处理异步任务,但对于长时间运行的任务,它不是最佳选择,因为它可能会导致主线程阻塞或出现其他不可预测的问题。
  • 多组件通信角度:Service 可以方便地与其他组件进行通信和交互,比如通过 startService () 方法启动后,可以通过 Intent 传递数据给 Service,Service 也可以通过广播或其他方式将执行结果反馈给其他组件。相比之下,Handler 主要用于线程间通信,在多组件之间的通信和协调方面,不如 Service 灵活和强大。

举例说明 Content Provider 在实际项目中的应用场景

  • 跨应用数据共享:假设开发了一个联系人管理应用和一个短信应用。短信应用需要获取联系人管理应用中的联系人信息来显示发件人的姓名等。通过 Content Provider,联系人管理应用可以将联系人数据暴露给其他应用,短信应用就可以使用 ContentResolver 来访问这些数据,从而实现跨应用的数据共享,让不同的应用能够协同工作,为用户提供更完整的功能体验。
  • 多用户数据隔离与共享:在一些具有多用户功能的应用中,如企业级的办公应用,不同用户有各自独立的文档、设置等数据。Content Provider 可以用于管理和提供这些数据,根据用户的标识来区分和提供不同用户的数据,同时也可以设置合适的权限,让某些数据可以在特定用户之间共享,保证数据的安全性和隐私性。
  • 第三方应用数据整合:当开发一个聚合新闻应用时,它需要整合多个新闻源的数据。这些新闻源可能来自不同的网站或服务提供商,通过 Content Provider,这些第三方应用可以将新闻数据以统一的格式提供给聚合新闻应用,聚合新闻应用则通过 ContentResolver 来获取和整合这些数据,为用户呈现丰富多样的新闻内容。

说明在 Android 中实现字数统计的颜色显示的方法

  • 使用 SpannableStringBuilder:可以先通过获取要显示的文本内容,然后计算其字数。接着使用 SpannableStringBuilder 来构建一个可设置样式的字符串。例如,根据字数的多少来设置不同的颜色。如果字数小于某个值,设置为绿色;如果字数在一定范围内,设置为黄色;如果字数超过某个较大值,设置为红色。通过设置 ForegroundColorSpan 来实现颜色的改变,然后将这个 SpannableStringBuilder 设置给 TextView 等用于显示文本的控件。
  • 自定义 TextView:创建一个继承自 TextView 的自定义视图类。在这个自定义视图中,重写 onDraw 方法。在 onDraw 方法中,先获取要显示的文本和其字数,然后根据字数来设置画笔的颜色,再使用画笔将文本绘制到画布上。这样,每次绘制文本时,都会根据字数动态地改变文本的颜色。
  • 结合 TextWatcher 和 Handler:给用于显示文本的 EditText 或 TextView 添加一个 TextWatcher,在 TextWatcher 的 afterTextChanged 方法中获取文本的字数。然后根据字数使用 Handler 发送一个延迟消息,在延迟消息的处理方法中,根据字数来设置文本的颜色。这样可以避免在用户输入过程中频繁地改变颜色,提高性能和用户体验。

解释 Android 中 ListView 控件的橡皮筋效果是如何实现的

  • 触摸事件处理:当用户在 ListView 上进行触摸操作时,ListView 会捕获并处理这些触摸事件。在触摸事件的处理方法中,根据手指的移动距离和方向等来判断是否触发了橡皮筋效果。例如,当手指向上滑动并超过了 ListView 的顶部边界时,就有可能触发橡皮筋效果。
  • 弹性动画实现:一旦确定触发了橡皮筋效果,ListView 会通过动画来实现弹性拉伸的效果。它会根据手指的移动距离,按照一定的比例来拉伸 ListView 的显示区域,通常是在顶部或底部添加一个额外的空白区域,让用户感觉列表像是被拉伸了一样。这个拉伸的过程是通过属性动画等方式来实现的,比如改变 ListView 的高度或顶部和底部的 padding 等属性。
  • 边界回弹效果:当用户手指松开后,ListView 需要根据当前的拉伸状态进行回弹。如果拉伸的距离较小,它会以一个较为平滑的动画效果回弹到原来的位置;如果拉伸的距离较大,可能会有一个稍微夸张一点的回弹效果,并且可能会伴随一些弹性的抖动,让回弹效果更加自然和生动,给用户一种类似于橡皮筋拉伸和回弹的感觉 。

详细说明 onSaveInstanceState () 的使用场景

onSaveInstanceState () 方法主要用于在 Activity 被系统销毁前保存当前 Activity 的一些状态信息,以便在 Activity 重新创建时能够恢复这些状态。

  • 屏幕旋转场景:当设备屏幕旋转时,默认情况下 Activity 会被销毁并重新创建。例如一个填写表单的 Activity,用户在旋转屏幕前已经输入了部分信息,如果不保存这些信息,旋转后用户将不得不重新输入。此时,onSaveInstanceState () 就可以将表单中的数据保存起来,在重新创建 Activity 时恢复数据,提升用户体验。
  • 系统内存不足被回收场景:当系统内存紧张时,可能会回收处于后台的 Activity。比如用户同时打开了多个应用,当前应用的 Activity 处于后台一段时间后,系统为了给前台应用腾出更多内存,可能会销毁该 Activity。通过 onSaveInstanceState () 保存 Activity 的状态,如列表的滚动位置、某些控件的选中状态等,当用户再次回到该 Activity 时,能看到之前的操作状态,而不是重新开始。
  • 应用进入后台后被系统杀死场景:有些情况下,应用进入后台一段时间后,系统可能会直接杀死应用进程以节省系统资源。在这种情况下,onSaveInstanceState () 保存的状态信息可以在下次启动应用进入该 Activity 时进行恢复,让用户感觉应用的连贯性和稳定性更好。

说明 ListView 优化的方法

  • 复用 convertView:在 ListView 的 getView () 方法中,不每次都创建新的 View,而是通过 convertView 参数复用之前创建的 View。这样可以减少 View 的创建次数,提高性能。例如,当列表项布局比较复杂时,创建 View 的开销较大,复用 convertView 能显著提升效率。
  • 使用 ViewHolder 模式:创建一个静态内部类 ViewHolder 来存储列表项中的各个 View 引用。在 getView () 方法中,通过 convertView 的 setTag () 和 getTag () 方法来设置和获取 ViewHolder,避免了每次都通过 findViewById () 来查找 View,减少了查找 View 的开销,进一步提升了性能。
  • 分页加载数据:当列表数据量较大时,一次性加载所有数据会导致内存占用过大和加载时间过长。采用分页加载,每次只加载当前屏幕显示及附近少量的数据,当用户滚动到接近底部时再加载下一页数据,这样可以有效减少内存消耗,提高列表的加载速度和流畅度。
  • 优化图片加载:如果列表项中有图片,不直接在主线程中加载图片,而是使用异步加载库,如 Glide、Picasso 等。这些库可以在后台线程中加载图片,并进行缓存管理,避免了在主线程中加载图片导致的卡顿,同时也减少了图片的重复加载。

列举平时使用过的开发框架

  • 网络请求框架:如 Retrofit,它是一个基于 OKHttp 的网络请求框架,通过接口和注解的方式来定义网络请求,使得网络请求的代码更加简洁和易于维护。它支持多种数据格式的解析,如 JSON、XML 等,并且可以方便地与其他框架集成,如 RxJava,用于处理异步操作和响应式编程。
  • 图片加载框架:除了上述提到的 Glide 和 Picasso,Glide 功能强大,支持多种图片格式,能高效地加载、缓存和展示图片,还可以对图片进行各种变换,如裁剪、圆角处理等。Picasso 则以简洁易用著称,它的 API 简单直观,也具备良好的缓存管理机制,适用于大多数图片加载场景。
  • 数据库框架:例如 GreenDAO,它是一个面向 Android 的轻量级数据库框架,提供了简单易用的 API 来进行数据库的创建、表的定义和数据的操作。它通过注解的方式来定义实体类与数据库表的映射关系,自动生成相应的数据库操作代码,大大提高了开发效率,并且性能较好,适合在 Android 应用中存储和管理本地数据。
  • 事件总线框架:如 EventBus,它是一种用于 Android 应用中组件间通信的框架,通过发布 / 订阅模式来实现事件的传递。在不同的 Activity、Fragment 或 Service 之间,可以方便地发送和接收事件,解耦了组件之间的依赖关系,使得代码更加松散和易于扩展。

阐述 Activity 启动模式以及 Service 种类及不同点

Activity 启动模式

  • standard 模式:这是 Activity 的默认启动模式。每次启动一个 Activity 时,都会创建一个新的实例并放入任务栈中。例如,从 Activity A 启动 Activity B,B 会进入任务栈成为栈顶元素,再从 B 启动 B,会再次创建一个新的 B 实例放入栈顶,这可能导致任务栈中有多个相同的 Activity 实例。这种模式适用于大多数普通的 Activity 启动场景,比如应用中的各个功能页面之间的正常跳转,每个页面都有独立的生命周期和状态。
  • singleTop 模式:如果要启动的 Activity 已经位于任务栈的栈顶,那么就不会再创建新的实例,而是直接复用栈顶的那个 Activity 实例,并调用其 onNewIntent () 方法。如果要启动的 Activity 不在栈顶,则会创建新的实例并放入栈顶。例如,在一个新闻应用中,新闻详情页可能采用 singleTop 模式,当用户在新闻详情页中点击分享按钮,分享完成后返回应用,如果新闻详情页还在栈顶,就直接复用,避免了重新创建和重新加载数据。
  • singleTask 模式:在整个任务栈中,只会存在一个该 Activity 的实例。如果要启动的 Activity 已经在任务栈中,那么系统会将该 Activity 之上的所有 Activity 都销毁,使它成为栈顶元素。例如,在一个应用中有一个登录 Activity,设置为 singleTask 模式,当用户在其他页面操作后需要重新登录时,启动登录 Activity,它会将之前在它之上的所有页面都销毁,回到登录页面,保证了登录状态的唯一性和独立性。
  • singleInstance 模式:该模式会为启动的 Activity 创建一个单独的任务栈,并且在整个系统中只有这一个实例。其他应用可以通过 Intent 来启动这个 Activity,但它始终在自己独立的任务栈中。比如,一些系统级的应用,如电话拨号界面,通常采用 singleInstance 模式,无论从哪个应用启动它,它都在自己独立的任务栈中,方便用户在不同应用间快速切换和使用。
Service 种类及不同点

  • 前台 Service:前台 Service 会在系统的通知栏显示一个持续的通知,以告知用户该 Service 正在运行。它通常用于执行一些用户需要明确知道其正在运行的任务,如音乐播放服务,即使应用切换到后台,用户也能通过通知栏的通知快速控制音乐的播放暂停等操作。前台 Service 的优先级较高,不容易被系统杀死,以保证任务的持续执行。
  • 后台 Service:后台 Service 在后台默默运行,不提供用户界面,主要用于执行一些不需要用户直接交互的任务,如文件下载、数据同步等。当应用退出后,后台 Service 可能会因为系统内存不足等原因被系统杀死。它的优先级相对较低,但可以通过一些策略来提高其在系统中的存活几率,如使用 startForeground () 方法将其提升为前台 Service,或者在 onStartCommand () 方法中返回合适的粘性值等。

阐述如何设计图片缓存框架

  • 缓存策略制定
    • 内存缓存:使用合适的内存数据结构,如 LruCache,来存储最近使用的图片。当加载图片时,首先从内存缓存中查找,如果找到则直接使用,大大提高了图片的获取速度。内存缓存的大小需要根据应用的内存使用情况合理设置,避免占用过多内存导致应用卡顿或被系统杀死。
    • 磁盘缓存:对于不经常使用但可能还会再次用到的图片,可以存储在磁盘缓存中。可以使用文件系统或者数据库来实现磁盘缓存,将图片以文件的形式存储在本地磁盘上,并建立相应的索引和管理机制。当内存缓存中找不到图片时,再从磁盘缓存中查找。
  • 图片加载与缓存逻辑设计
    • 异步加载:为了避免在主线程中加载图片导致界面卡顿,采用异步加载的方式。可以使用线程池来管理加载图片的线程,当有图片需要加载时,从线程池中获取一个线程来执行加载任务。在加载完成后,将图片存储到内存缓存和磁盘缓存中,并通知 UI 线程更新界面。
    • 缓存更新与淘汰机制:随着应用的使用,缓存的图片可能会越来越多,需要设计合理的缓存更新和淘汰机制。对于内存缓存,可以根据图片的最近使用时间或使用频率来淘汰不常用的图片;对于磁盘缓存,可以根据缓存的大小、图片的最后访问时间等因素来删除一些旧的图片,以保证缓存的有效性和空间利用率。
  • 图片处理与优化
    • 图片压缩:在加载图片时,根据显示需求对图片进行适当的压缩,以减少内存占用和提高加载速度。可以根据图片的显示尺寸和分辨率,采用合适的压缩算法,如 BitmapFactory 的 Options 参数来进行图片的采样压缩,在不影响图片显示效果的前提下,降低图片的质量和大小。
    • 图片格式支持:支持多种图片格式,如 JPEG、PNG、GIF 等,以满足不同的图片来源和显示需求。对于不同格式的图片,采用相应的解码和处理方式,确保图片能够正确加载和显示。
  • 与其他组件的交互与集成
    • 与 ListView、RecyclerView 等控件的结合:在列表中显示图片时,与相应的列表控件良好结合,实现图片的异步加载和缓存。当列表项滚动时,及时回收不可见的图片资源,避免内存泄漏,并在需要显示图片时从缓存中快速获取或异步加载。
    • 与网络请求框架的协同工作:如果图片需要从网络获取,与网络请求框架配合,在网络请求成功后将图片存储到缓存中,并在网络请求失败时尝试从缓存中获取图片,以提供更好的用户体验和数据可用性。

描述 TCP 三次握手的过程,并解释为什么要进行三次握手

  • TCP 三次握手过程

    • 第一次握手:客户端向服务器发送一个 SYN(同步序列号)包,其中包含客户端随机生成的初始序列号(ISN),并将 SYN 标志位置为 1,表示请求建立连接。这个包的目的是告诉服务器客户端想要建立连接,并告知服务器自己的初始序列号,以便服务器在后续的通信中能够对数据包进行正确的排序和确认。
    • 第二次握手:服务器收到客户端的 SYN 包后,会返回一个 SYN/ACK 包作为应答。这个包中,SYN 标志位仍然为 1,表示服务器也同意建立连接,同时它也会生成一个自己的初始序列号,并将其包含在包中。ACK 标志位也被置为 1,表示对客户端的 SYN 包进行确认,确认号为客户端的初始序列号加 1 ,表示服务器已经收到了客户端的序列号,并期望下一次收到的序列号是客户端初始序列号加 1。
    • 第三次握手:客户端收到服务器的 SYN/ACK 包后,会向服务器发送一个 ACK 包作为最后的确认。这个包的 ACK 标志位为 1,确认号为服务器的初始序列号加 1,表示客户端已经收到了服务器的序列号,并确认连接建立成功。此时,客户端和服务器都已经知道了对方的初始序列号,并且都确认了连接的建立,双方可以开始进行数据传输。

  • 为什么要进行三次握手

    • 确认双方的接收和发送能力:第一次握手,客户端发送 SYN 包,服务器收到后可以确认客户端有发送能力,同时服务器返回 SYN/ACK 包,客户端收到后可以确认服务器有接收和发送能力。第三次握手,客户端发送 ACK 包,服务器收到后可以确认客户端有接收能力。通过三次握手,双方都能确认对方的接收和发送能力正常,从而保证后续数据传输的可靠性。
    • 防止已失效的连接请求报文段突然又传送到了服务端:假设只有两次握手,如果客户端发送的第一个 SYN 包在网络中延迟或丢失,客户端会因为长时间未收到服务器的确认而重新发送 SYN 包。当服务器收到重新发送的 SYN 包并建立连接后,之前延迟的那个 SYN 包可能会在某个时刻又到达服务器,如果没有第三次握手来确认客户端是否真的想要建立新的连接,服务器就会错误地建立一个无效的连接,浪费系统资源。而通过三次握手,客户端可以通过第三次握手的 ACK 包来确认当前的连接请求是最新有效的,避免了这种情况的发生。

阐述 TCP 四次挥手的过程,并说明为什么是四次挥手

  • TCP 四次挥手过程

    • 第一次挥手:主动关闭方(通常是客户端)发送一个 FIN(结束标志)包,其中 FIN 标志位被置为 1,表示客户端想要关闭连接。此时客户端进入 FIN_WAIT_1 状态,等待服务器的确认。
    • 第二次挥手:服务器收到客户端的 FIN 包后,会发送一个 ACK 包作为应答,确认号为客户端的 FIN 包的序列号加 1,表示服务器已经收到了客户端的关闭请求。此时服务器进入 CLOSE_WAIT 状态,客户端收到服务器的 ACK 包后进入 FIN_WAIT_2 状态。
    • 第三次挥手:服务器在处理完剩余的数据后,也会发送一个 FIN 包给客户端,表示服务器也准备关闭连接。此时服务器进入 LAST_ACK 状态,等待客户端的确认。
    • 第四次挥手:客户端收到服务器的 FIN 包后,会发送一个 ACK 包作为最后的确认,确认号为服务器的 FIN 包的序列号加 1。客户端进入 TIME_WAIT 状态,经过一段时间的等待(通常是 2MSL,MSL 是最长报文段寿命)后,客户端关闭连接,进入 CLOSED 状态。服务器收到客户端的 ACK 包后,也立即关闭连接,进入 CLOSED 状态。

  • 为什么是四次挥手

    • 确保数据传输的完整性:TCP 是全双工通信协议,意味着双方都可以同时进行数据的发送和接收。当一方想要关闭连接时,它只是表示自己不再发送数据,但可能还需要接收对方尚未发送完的数据。因此,第一次挥手时,主动关闭方发送 FIN 包只是表示自己不再发送数据,但还可以接收数据。第二次挥手,服务器返回 ACK 包确认收到了主动关闭方的关闭请求,同时告知主动关闭方可以继续接收数据。第三次挥手,服务器发送 FIN 包表示自己也不再发送数据,此时双方都确认了不再发送数据。第四次挥手,主动关闭方发送 ACK 包确认收到了服务器的 FIN 包,至此双方都确认了连接的关闭,这样可以确保在关闭连接前,双方的数据都已经传输完毕,保证了数据传输的完整性。
    • 防止数据丢失和错误关闭:如果只有三次挥手,可能会出现这样的情况:服务器在收到客户端的 FIN 包后,立即发送自己的 FIN 包并关闭连接,但此时服务器可能还有一些数据正在发送给客户端的路上,如果直接关闭连接,这些数据就会丢失。而通过四次挥手,服务器在收到客户端的 FIN 包后,先进入 CLOSE_WAIT 状态,继续发送剩余的数据,等数据发送完后再发送 FIN 包关闭连接,避免了数据丢失和错误关闭的问题 。

详细说明访问一个网页的完整过程

当在浏览器中输入网址并按下回车键后,首先会进行域名解析。浏览器会向本地域名服务器发送请求,查询目标网址对应的 IP 地址 。如果本地域名服务器没有缓存该域名的 IP 地址,它会向根域名服务器发起查询,根域名服务器会告知本地域名服务器应该向哪个顶级域名服务器查询,然后顶级域名服务器再告知负责该域名的权威域名服务器,最终获取到目标网址的 IP 地址。

接着,浏览器会与目标服务器建立 TCP 连接。通过三次握手过程,确保双方都准备好进行数据传输,以建立可靠的连接。连接建立后,浏览器会向服务器发送 HTTP 请求,请求中包含请求方法(如 GET、POST 等)、请求的网址、协议版本以及一些请求头信息,如浏览器类型、接受的编码格式等。

服务器收到请求后,根据请求的内容进行相应的处理。如果请求的是静态资源,如 HTML 文件、图片等,服务器会直接读取并返回这些资源;如果是动态请求,服务器会执行相应的脚本程序,生成动态内容并返回。

服务器返回的响应信息包含状态码,如 200 表示成功、404 表示未找到资源等,以及响应头信息,如内容类型、内容长度等,还有响应的正文内容,也就是网页的代码或数据。

浏览器收到响应后,会根据响应头中的内容类型来解析正文内容。如果是 HTML 文件,浏览器会开始渲染页面,依次解析 HTML 标签,构建 DOM 树,同时加载和解析 CSS 样式表,构建 CSSOM 树,然后将 DOM 树和 CSSOM 树合并成渲染树,最后根据渲染树进行布局和绘制,将网页呈现给用户。在渲染过程中,如果遇到需要加载的外部资源,如图片、脚本等,浏览器会再次发起请求去获取这些资源,直到整个网页完全加载和显示出来。

已知 http 是基于 tcp 的,解释为什么 tcp 可以承载 http

TCP(传输控制协议)之所以能够承载 HTTP(超文本传输协议),主要是因为 TCP 具备一系列适合承载 HTTP 的特性。

首先,TCP 是一种面向连接的协议。在数据传输之前,它会通过三次握手建立可靠的连接,确保双方都准备好进行数据交互。这对于 HTTP 来说非常重要,因为 HTTP 的请求和响应需要在一个稳定、可靠的连接上进行传输,以保证数据的完整性和准确性。例如,当我们访问一个网页时,浏览器与服务器之间通过 TCP 建立连接后,HTTP 的请求和服务器返回的响应才能在这个连接上有序地传输,避免数据丢失或错误。

其次,TCP 提供了可靠的字节流服务。它通过序列号、确认应答、重传机制等确保数据能够准确无误地从发送端传输到接收端。在 HTTP 传输中,无论是请求消息还是响应消息,都需要以正确的顺序和完整的内容到达对方。TCP 的可靠性保证了 HTTP 数据的正确传输,比如一个网页的 HTML 代码、图片等数据都能准确地从服务器传输到浏览器。

再者,TCP 具有流量控制和拥塞控制机制。流量控制可以防止发送端发送数据过快,导致接收端缓冲区溢出;拥塞控制则可以避免网络出现拥塞,保证网络的稳定性和性能。在承载 HTTP 时,这些机制可以确保在网络状况不佳或服务器负载较高时,数据传输依然能够平稳进行,不会因为大量的 HTTP 请求导致网络瘫痪或服务器崩溃。

说明在项目中涉及的设计模式,以及是否经常看源代码,阐述多看看源代码对编程思维的帮助

在项目开发中,常常会涉及多种设计模式。例如单例模式,它确保一个类只有一个实例,并提供一个全局访问点。在很多场景中都非常有用,比如在 Android 开发中,对于一些全局的配置信息管理类、数据库连接类等,使用单例模式可以避免创建多个实例造成资源浪费和数据不一致的问题。

工厂模式也是常用的一种,它将对象的创建和使用分离。比如在创建不同类型的数据库操作对象时,可以使用工厂模式,根据不同的需求创建相应的数据库操作对象,提高了代码的可维护性和可扩展性。

观察者模式同样应用广泛,它定义了一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都会收到通知并自动更新。在 Android 的 UI 开发中,当数据发生变化时,通过观察者模式可以及时更新 UI 界面,保持数据和界面的一致性。

关于是否经常看源代码,这是一个非常好的编程习惯。我经常会查看一些优秀开源项目的源代码,以及 Android 系统框架的部分源代码。

多看看源代码对编程思维有很大的帮助。首先,它能让我们学习到优秀的代码结构和设计思路。通过阅读源代码,我们可以看到别人是如何组织代码、如何划分模块、如何实现各种功能的,从而借鉴这些经验来优化自己的代码结构。其次,有助于理解各种设计模式的实际应用。在源代码中,我们可以看到设计模式是如何巧妙地解决实际问题的,加深对设计模式的理解和掌握。再者,能够提升解决问题的能力。当遇到问题时,通过查看相关的源代码,我们可以了解到问题的底层实现机制,从而更好地找到解决问题的方法。最后,看源代码还能拓宽知识面,了解到一些新的技术和算法,为我们的项目开发提供更多的思路和可能性。

解释为什么离职,阐述自己的职业目标规划以及预计什么时候能入职

离职的原因通常是多方面的。一方面可能是原公司的发展方向与个人职业规划不太相符。随着个人技术的成长和对自身职业发展的深入思考,发现原公司所提供的业务领域和技术挑战逐渐无法满足自己的需求,希望寻找一个更能发挥自身技术优势和实现职业目标的平台。

另一方面,可能是希望在技术上有更深入的学习和提升。原公司的技术栈相对固定,缺乏接触新技术和新架构的机会,为了不断提升自己的技术水平,拓展技术视野,所以选择离职去寻找更具挑战性的技术环境。

关于职业目标规划,从短期来看,希望能够快速融入新的团队,熟悉公司的业务和技术架构,承担起相应的工作职责,在项目中不断提升自己的技术能力,尤其是在 Android 开发的性能优化、架构设计等方面取得进步。

中期目标是能够成为团队中的核心成员,参与到重要项目的决策和开发中,带领小组完成一些具有挑战性的任务,同时不断学习和掌握新的技术,提升自己的综合素质,如沟通能力、团队协作能力等。

长期目标则是希望能够在技术领域有一定的影响力,通过不断积累项目经验和技术成果,能够参与技术分享和开源社区的建设,为行业的发展做出自己的贡献。

至于预计入职时间,一般在收到正式的录用通知后,如果没有特殊情况,我可以在 [X] 周内完成原公司的离职手续并入职新公司。当然,具体时间还会根据双方的沟通和实际情况进行灵活调整。

描述在工作中遇到的困难以及主要的解决办法

在工作中遇到过不少困难,其中一个比较典型的是在开发一个复杂的 Android 应用时,遇到了性能优化方面的问题。随着应用功能的不断增加,界面变得越来越卡顿,启动速度也明显变慢。

为了解决这个问题,首先进行了性能分析。使用了一些性能分析工具,如 Android Profiler,来监测应用在运行过程中的 CPU、内存、网络等使用情况,通过分析发现了一些代码中的性能瓶颈,比如在某些页面存在大量的重复绘制操作,以及一些资源的不合理加载和使用。

针对重复绘制问题,对布局进行了优化。减少了不必要的嵌套布局,尽量使用更高效的布局方式,如 ConstraintLayout,避免了过度绘制。对于资源的加载,采用了懒加载的方式,只在需要使用资源时才进行加载,避免了一次性加载大量资源导致的内存占用过高和启动速度慢的问题。

另外,还遇到过与团队成员在技术方案上的分歧。在开发一个新功能时,对于采用哪种技术架构和设计方案,团队成员有不同的意见。为了解决这个问题,我们组织了多次技术讨论会议,每个人都详细阐述了自己的方案的优缺点和技术细节。然后,一起对各个方案进行了评估和对比,从技术可行性、开发成本、维护成本等多个角度进行分析,最终达成了共识,选择了一个综合最优的方案。

解释全局广播和本地广播的区别。

全局广播可以被系统中任何拥有相应权限的应用接收到。它是通过 Android 系统的广播机制来发送和接收信息的。当一个应用发送一个全局广播,只要其他应用注册了对应的广播接收器并且有相应的权限,就能够接收到这个广播消息。例如,系统开机广播是一个全局广播,当手机开机时,系统会发送这个广播,很多应用可以注册接收这个广播来进行一些初始化的操作,像启动一些后台服务等。

全局广播使用的是标准的 Intent 进行广播发送,它可以跨应用传递消息,但是也正因如此,它存在一定的安全风险。恶意应用可能会监听某些关键的广播,从而获取用户的隐私信息或者干扰系统的正常运行。

本地广播则不同,它仅限于应用内部使用。本地广播是通过 LocalBroadcastManager 来管理的。它的广播消息只能在本应用的组件之间传递,其他应用无法接收到。这种方式增强了安全性,因为广播不会泄露到应用外部。而且,本地广播相对来说更加高效,因为它不需要经过系统的广播队列进行调度,减少了广播传递的开销。例如,在一个复杂的应用中,某个 Activity 要通知同一应用内的 Service 进行数据更新,就可以使用本地广播,这样既可以实现组件间的通信,又避免了外部应用的干扰。

详述 Activity 的生命周期,包括每个阶段的特点以及通常要做的事情。

Activity 的生命周期主要包括以下几个阶段:

首先是 onCreate () 阶段。这个阶段是在 Activity 第一次被创建的时候调用。此时系统会创建 Activity 的实例,并且会调用 onCreate () 方法。在这个方法中,通常会进行一些初始化的操作,比如设置布局文件(通过 setContentView () 方法),初始化一些视图组件,还可以进行一些数据的初始化操作。例如,如果 Activity 需要从数据库或者网络获取数据来显示,就可以在这个阶段发起数据请求。同时,也可以在这里绑定一些服务,比如传感器服务等。

接着是 onStart () 阶段。在这个阶段,Activity 已经变得可见了,但是还没有获取焦点,用户还不能和 Activity 进行交互。这个阶段主要是系统在进行一些准备工作,使得 Activity 能够完全展示在用户面前。通常在这个阶段,可以进行一些和视图显示相关的操作,比如更新视图的一些状态显示,但是因为还没有获取焦点,所以像一些输入框获取焦点之类的操作是不合适的。

然后是 onResume () 阶段。此时 Activity 已经获取了焦点,用户可以正常地和 Activity 进行交互了。这是 Activity 处于前台并且正在运行的阶段。在这个阶段,通常会进行一些和用户交互相关的操作,比如开始动画效果,启动一些需要和用户交互的定时任务等。例如,如果是一个游戏应用,游戏的主要逻辑循环可能会在这个阶段开始运行。

当 Activity 失去焦点但是仍然可见时,会进入 onPause () 阶段。在这个阶段,系统可能会暂停一些动画或者正在进行的媒体播放(如视频或者音频)。因为这个时候 Activity 可能会很快被销毁或者重新回到前台,所以在这里应该快速地保存一些关键的数据,比如用户当前的输入状态等。例如,在一个文档编辑应用中,如果用户正在编辑文档,当接收到电话或者其他通知导致 Activity 失去焦点时,应该在这个阶段保存用户已经编辑的内容,防止数据丢失。

当 Activity 完全不可见时,会进入 onStop () 阶段。在这个阶段,Activity 已经不在前台了,可能是被其他 Activity 完全覆盖或者应用进入了后台。这个时候可以释放一些占用的资源,比如停止一些后台线程或者解除和服务的绑定(如果不需要在后台继续运行的话)。不过要注意,如果在 onStop () 阶段之后 Activity 又重新回到前台,系统会调用 onRestart () 方法,然后再经过 onStart () 和 onResume () 方法让 Activity 重新回到运行状态,所以在释放资源的时候要考虑到这种情况,避免过度释放导致重新显示时出现问题。

最后是 onDestroy () 阶段。这个阶段是 Activity 被销毁的时候调用。通常在这个阶段,应该释放所有占用的资源,包括解除和其他组件的引用,关闭数据库连接等。例如,如果 Activity 在运行过程中创建了一些自定义的对象,并且这些对象占用了大量的内存,在这个阶段就需要对这些对象进行释放,确保系统资源能够被有效利用。

当 Activity B 覆盖了 Activity A 时,阐述 Activity A 生命周期的具体变化情况。

当 Activity B 覆盖 Activity A 时,Activity A 会经历一系列的生命周期变化。

首先,Activity A 会进入 onPause () 阶段。在这个阶段,Activity A 失去焦点,但是仍然可见。此时,Activity A 应该暂停一些正在进行的操作,例如动画或者媒体播放等。同时,应该快速地保存一些关键的数据,比如用户的输入状态等。因为这个时候 Activity A 可能会很快被销毁,也可能在 Activity B 退出后重新回到前台。

接着,如果 Activity B 完全覆盖了 Activity A,Activity A 会进入 onStop () 阶段。在这个阶段,Activity A 已经完全不可见了。此时可以释放一些占用的资源,例如停止一些后台线程或者解除和服务的绑定(如果不需要在后台继续运行的话)。不过,需要注意的是,如果在之后 Activity A 又要重新回到前台,系统会先调用 onRestart () 方法,然后再经过 onStart () 和 onResume () 方法让 Activity A 重新回到运行状态,所以在释放资源的时候要谨慎考虑,避免过度释放导致重新显示时出现问题。

说明广播在 Android 中的两种注册方式,并比较它们的特点。

在 Android 中,广播有两种注册方式,动态注册和静态注册。

动态注册是在代码中通过调用 registerReceiver () 方法来实现广播接收器的注册。这种注册方式的特点是比较灵活。广播接收器的注册和注销可以在程序运行过程中根据需要进行控制。例如,在一个应用中,只有当用户进入某个特定的功能模块时,才需要接收某个广播,就可以在用户进入这个模块时动态地注册广播接收器,当用户离开这个模块时,再注销这个广播接收器。动态注册的广播接收器是和注册它的组件(如 Activity、Service 等)的生命周期相关联的。当组件被销毁时,广播接收器也会随之被注销。这就要求在组件的生命周期方法中,比如 onDestroy () 方法中,要正确地注销广播接收器,以避免内存泄漏等问题。

静态注册是通过在 AndroidManifest.xml 文件中进行配置来实现广播接收器的注册。这种注册方式的优点是广播接收器可以在应用没有启动的情况下接收到广播。例如,对于一些系统广播,如开机广播,应用可以通过静态注册广播接收器来接收这个广播,并且在接收到广播后可以启动一些后台服务或者进行其他初始化操作。不过,静态注册的广播接收器相对来说不够灵活,一旦在清单文件中配置了,它就会一直存在,直到应用被卸载。而且,如果应用没有正确地处理好静态注册的广播接收器,可能会导致一些性能问题或者安全问题,比如接收过多不必要的广播导致资源浪费,或者被恶意利用来获取用户信息等。

详述 Android 中 touch 的事件分发机制。

在 Android 中,touch 事件的分发机制主要涉及三个重要的方法:dispatchTouchEvent ()、onInterceptTouchEvent () 和 onTouchEvent ()。

当一个触摸事件发生时,首先会调用最顶层 View 的 dispatchTouchEvent () 方法。这个方法的主要作用是将触摸事件分发给它的子 View 或者自己来处理。它会按照一定的顺序来判断是否要将事件传递下去。如果这个 View 是一个 ViewGroup(即可以包含其他子 View 的容器视图),它会首先调用自己的 onInterceptTouchEvent () 方法来判断是否要拦截这个触摸事件。

onInterceptTouchEvent () 方法在 ViewGroup 中用于决定是否拦截触摸事件。如果这个方法返回 true,那么后续的触摸事件(如 MOVE、UP 等)将不会再传递给它的子 View,而是由这个 ViewGroup 自己来处理。例如,在一个自定义的滑动布局中,如果希望在滑动一定距离后拦截触摸事件,不让子 View 处理,就可以在 onInterceptTouchEvent () 方法中根据触摸的位移来判断是否拦截。

如果 ViewGroup 没有拦截触摸事件(即 onInterceptTouchEvent () 返回 false),那么触摸事件会按照视图树的层次结构依次传递给子 View 的 dispatchTouchEvent () 方法。这个过程是递归的,直到找到一个能够处理这个触摸事件的 View。

当触摸事件传递到一个 View(无论是 ViewGroup 还是普通的 View)时,会调用这个 View 的 onTouchEvent () 方法来处理触摸事件。如果这个方法返回 true,表示这个 View 已经成功地处理了这个触摸事件,那么后续的触摸事件(对于同一个触摸序列)将继续传递给这个 View 来处理。例如,对于一个按钮的点击事件,当手指按下(DOWN)、移动(MOVE)和抬起(UP)的过程中,按钮的 onTouchEvent () 方法会根据这些触摸事件来改变自己的状态,比如按下时改变背景颜色,抬起时触发点击事件的逻辑等。

如果一个 View 的 onTouchEvent () 方法返回 false,表示这个 View 不能处理这个触摸事件,那么这个触摸事件会向上回溯,传递给它的父 View 的 onTouchEvent () 方法来处理,这个过程也是递归的,直到有一个 View 能够成功地处理这个触摸事件或者事件被丢弃(如果一直没有 View 能够处理)。

同时,在整个触摸事件分发过程中,还可能会涉及到一些其他的因素,比如触摸事件的类型(如单点触摸、多点触摸)、视图的状态(如是否可点击、是否可见等)等都会影响事件的分发和处理。而且,开发者还可以通过设置一些标志位或者使用触摸事件的监听器来进一步控制触摸事件的分发和处理,以满足不同的应用需求。


http://www.kler.cn/a/416690.html

相关文章:

  • 根据导数的定义计算导函数
  • 【Git 工具】用 IntelliJ IDEA 玩转 Git 分支与版本管理
  • 哈希表,哈希桶的实现
  • 气膜建筑:打造全天候安全作业空间,提升工程建设效率—轻空间
  • 自编码器(二)
  • 嵌入式QT学习第4天:Qt 信号与槽
  • 【查询目录】.NET开源 ORM 框架 SqlSugar 系列
  • 在VMware虚拟机上安装Kali Linux的详细教程(保姆级教程)
  • 腾讯微众银行大数据面试题(包含数据分析/挖掘方向)面试题及参考答案
  • 鸿蒙NEXT元服务:利用App Linking实现无缝跳转与二维码拉起
  • Linux系统编程——进程替换
  • Python异步编程新写法:asyncio模块的最新实践
  • K8s的API资源对象NetworkPolicy
  • BiGRU:双向门控循环单元在序列处理中的深度探索
  • SAP Native SQL 的简单说明
  • C#里怎么样LINQ来从数组里获得大于某数的值?
  • GPT诞生两周年,AIPC为连接器带来什么新变化?
  • vue3 ts button 组件封装
  • Docker 清理镜像策略详解
  • Vue3组件通信的8种方式,完整源码带注释
  • 图解:XSS攻击原理与安全过滤
  • ip租期到了
  • Qt关于padding设置不起作用的的解决办法
  • 【C++】泛型算法(三):定制操作
  • Android 图形系统之三:SurfaceControl
  • aws rds-mysql不支持性能详情监控