知识储备
1、很多人准备面试的时候,大多数就会这样的,打开牛客网,查找 “Android 面经”,找几个多的开始看。以及从各大技术交流平台搜索Android面试题。开始刷题复习。
2、这虽然看起来很自然,但是并不是一种 健康的方式,面试题的作用应当是查缺补漏,上学的时候也不是直接发习题册然后对着答案学习吧?
3、知乎上有个问题,叫做 如何高效学习,里面有一个回答我觉得很符合我自己的观点,就是 建立起自己的知识体系,建立知识体系的目的在于:
巩固记忆。
认识自己,找到自己的优势及不足。
把握复习的进度。
经验总结。
目前我参考和自己整理的总结的会分为以下几个大点:
一、 Java 相关
容器(HashMap、HashSet、LinkedList、ArrayList、数组等)
HashMap和HashSet是Java集合框架中常用的两种数据结构,它们都实现了Set接口,但在使用方式和内部实现上存在一些区别。
HashMap:
HashMap是一个键值对(Key-Value)存储的数据结构。
它使用哈希表的方式来存储和访问数据,通过将键映射到哈希码来加快数据的查找速度。
HashMap允许使用null作为键和值,并且是非线程安全的。
它提供了常数时间(O(1))的平均性能用于插入、删除和查找操作。
HashMap的键是唯一的,如果插入重复的键,则会覆盖旧值。
HashMap的元素没有特定的顺序,遍历时不保证元素的顺序。
HashSet:
HashSet是一个基于哈希表的Set实现,它存储唯一的元素集合。
HashSet不允许重复元素,如果插入重复的元素,后续的插入操作将会被忽略。
它允许使用null作为元素,并且是非线程安全的。
HashSet提供了常数时间(O(1))的平均性能用于插入、删除和查找操作。
HashSet的元素没有特定的顺序,遍历时不保证元素的顺序。
区别:
数据结构:HashMap是键值对的映射表,而HashSet是唯一元素的集合。
存储方式:HashMap使用键值对的形式存储数据,而HashSet仅存储元素。
元素唯一性:HashMap中键是唯一的,而HashSet中元素是唯一的。
插入顺序:HashMap中的元素没有特定的顺序,而HashSet中的元素也没有特定的顺序。
使用方式:HashMap适用于需要通过键值对进行数据访问的场景,而HashSet适用于需要存储唯一元素的场景。
根据具体的需求,选择HashMap还是HashSet会根据是否需要键值对的映射关系以及是否需要存储重复元素来决定。
LinkedList 和 ArrayList 是 Java 集合框架中的两种常见数据结构,它们在 Android 中也得到广泛应用。以下是它们的原理和区别:
LinkedList:
LinkedList 是一个双向链表的实现,每个节点包含对前一个节点和后一个节点的引用。
在内存中,每个节点都是通过指针链接在一起的。
当插入或删除节点时,只需要调整节点之间的引用,而不需要移动其他节点。
链表的访问时间复杂度为 O(n),其中 n 是索引位置,需要遍历链表来到达目标位置。
由于维护节点之间的引用关系,链表的内存消耗相对较高。
ArrayList:
ArrayList 是一个基于数组的实现,内部使用数组来存储元素。
在内存中,连续的存储空间用于存储元素。
当插入或删除元素时,需要进行数组的移动操作,以保证元素的连续性。
随机访问元素的时间复杂度为 O(1),因为可以通过索引直接访问数组中的元素。
由于不需要维护节点之间的引用关系,ArrayList 的内存消耗相对较低。
区别:
数据结构:LinkedList 是双向链表,ArrayList 是基于数组。
插入和删除操作:LinkedList 在插入和删除节点时开销较小,只需调整节点的引用;ArrayList 在中间位置插入或删除元素时开销较大,需要进行数组的移动操作。
随机访问和遍历操作:ArrayList 适用于频繁的随机访问和遍历操作,因为可以通过索引直接访问元素;LinkedList 需要遍历链表来访问指定位置的元素。
内存消耗:LinkedList 的内存消耗相对较高,因为需要维护节点之间的引用关系;ArrayList 的内存消耗相对较低,只需要存储元素本身和数组的容量。
根据具体的需求,选择 LinkedList 还是 ArrayList 取决于对数据的操作方式。如果需要频繁的插入和删除操作,可以选择 LinkedList。如果需要频繁的随机访问和遍历操作,可以选择 ArrayList。
内存模型
Android 的内存模型是指在 Android 应用程序中,分配、管理和使用内存的机制。Android 的内存模型可以分为以下几个方面:
Java 堆(Java Heap):Java 堆是用于存储对象实例的内存区域。在 Android 应用程序中,所有通过关键字 new 创建的对象都存储在 Java 堆中。Java 堆是由垃圾回收器(Garbage Collector)负责管理的,它会自动释放不再使用的对象。
栈(Stack):栈是存储方法调用和局部变量的内存区域。每个线程在运行时都会有一个对应的栈,用于保存方法调用过程中的局部变量和临时数据。栈上的数据在方法结束后会立即释放,并且栈的大小在编译时就确定了。
Native 堆(Native Heap):Native 堆是用于存储通过 JNI(Java Native Interface)调用 native 代码创建的对象和数据的内存区域。Native 堆不受 Java 垃圾回收器的管理,需要手动释放内存。
常量池(Constant Pool):常量池是存储字符串常量和符号引用的内存区域。它包括类、方法和字段的符号引用信息,以及字符串常量。常量池在编译时就确定了,并且存储在类加载过程中。
堆外内存(Off-Heap Memory):堆外内存是指分配在系统堆之外的内存区域。在 Android 中,可以使用一些特殊的 API(如 ByteBuffer)来操作堆外内存。堆外内存不受 Java 垃圾回收器的管理,需要手动释放。
内存优化:由于移动设备的内存资源有限,Android 开发中需要注意内存的优化。一些常见的内存优化技术包括使用轻量级对象、避免内存泄漏、及时释放资源、使用适当的数据结构等。
Android 的内存模型对于应用程序的性能和稳定性非常重要。合理地管理内存可以减少内存占用、避免内存泄漏和性能问题,提升应用程序的响应速度和用户体验。开发者需要了解 Android 的内存模型,并根据实际应用场景进行内存优化和资源管理。
垃圾回收算法(JVM)
在 Android 中,主要使用以下几种垃圾回收算法:
标记-清除算法(Mark and Sweep):这是一种基本的垃圾回收算法,它通过两个阶段进行。首先,标记阶段通过从根对象(如活动对象、全局变量等)出发,递归地遍历对象图,并标记所有可达对象。然后,在清除阶段,未被标记的对象被认为是垃圾,将其回收并释放内存空间。标记-清除算法的主要缺点是会产生内存碎片。
复制算法(Copying):复制算法将堆内存分为两个区域,通常为相等大小的年轻代区域。在每次垃圾回收时,只有一个区域是活动的,称为“From”区域,而另一个区域是空闲的,称为“To”区域。在标记阶段,活动区域中的存活对象被复制到空闲区域,然后清除整个活动区域。复制算法消除了内存碎片问题,但需要额外的空间来存储复制对象。
标记-整理算法(Mark and Compact):标记-整理算法结合了标记-清除算法和复制算法的优点。它首先进行标记阶段,标记所有可达对象。然后,在清除阶段,将存活对象向一端移动,并按照地址顺序进行紧凑排列,以消除内存碎片。标记-整理算法需要进行内存移动,因此可能会在性能上略微有所影响。
分代回收算法(Generational Collection):分代回收算法是针对对象生命周期的特点进行的优化。它将堆内存分为不同的代,通常包括年轻代和老年代。年轻代存放新创建的对象,而老年代存放经过多次回收仍然存活的对象。分代回收器使用不同的策略和频率来处理不同代的对象,以提高垃圾回收的效率。
这些垃圾回收算法在 Android 的虚拟机实现中相互组合和优化,例如在 Dalvik 虚拟机和 ART(Android Runtime)中。其中,ART 引入了一些额外的优化技术,如延迟标记、增量式垃圾回收和并发标记等,以减少垃圾回收对应用程序的停顿时间和性能影响。
需要注意的是,具体采用哪种垃圾回收算法取决于虚拟机的实现和配置,以及设备的硬件性能和内存约束。不同版本的 Android 和不同的设备可能会有不同的垃圾回收策略。
类加载过程(需要多看看,重在理解,对于热修复和插件化比较重要)
在 Android 中,类加载过程主要分为以下几个步骤:
加载(Loading):加载是类加载过程的第一步,它负责查找并加载类的字节码文件。在 Android 中,类的字节码通常存储在.dex(Dalvik Executable)文件中,或者在较新的 Android 版本中存储在ART(Android Runtime)的.oat(Ahead-Of-Time)文件中。类加载器根据类的全限定名(例如 com.example.MyClass)查找相应的字节码文件。
链接(Linking):
a. 验证(Verification):验证阶段对加载的字节码进行验证,确保其符合 Java 虚拟机规范和安全性要求。验证过程包括静态分析、字节码验证、符号引用验证等。
b. 准备(Preparation):准备阶段为类的静态变量分配内存空间,并设置默认初始值。这些静态变量包括基本类型和引用类型。
c. 解析(Resolution):解析阶段将符号引用转换为直接引用。符号引用是对类、方法或字段的引用,而直接引用是指向内存地址的指针或句柄。
初始化(Initialization):初始化阶段是类加载过程的最后一步。在这个阶段,类的静态变量被赋予程序中定义的初始值,静态代码块被执行,以及执行其他必要的初始化操作。这一阶段可以包括对静态变量的赋值、静态代码块的执行和调用父类构造函数等。
需要注意的是,类加载过程在首次使用类时才会触发,而且每个类只会加载一次。在 Android 中,类加载是由虚拟机负责执行的,具体由 Dalvik 虚拟机或 ART(Android Runtime)负责。ART 引入了一种称为预编译(Ahead-Of-Time Compilation)的技术,将字节码转换为本地机器代码,以提高应用程序的性能。因此,在 ART 中,类加载过程与 Dalvik 虚拟机略有不同,但整体流程是类似的。
总结起来,Android 中的类加载过程包括加载、链接和初始化阶段。通过这个过程,Android 能够在运行时动态加载和使用类,并为其分配内存空间以及执行必要的初始化操作。
反射
在 Android 中,反射(Reflection)是一种机制,允许在运行时检查和操作类、对象、方法和字段的信息。通过反射,可以动态地获取类的信息、创建对象、调用方法和访问字段,而无需在编译时明确地引用它们。
反射的实现原理如下:
获取类的信息:通过调用Class.forName("com.example.MyClass")方法,可以通过类的全限定名获取对应的Class对象。Class对象提供了获取类的信息,如构造函数、方法和字段的信息。
创建对象:通过Class对象的newInstance()方法,可以创建类的实例对象。这相当于调用类的默认构造函数来实例化对象。
获取方法和字段:通过Class对象的getMethod()、getDeclaredMethod()、getField()和getDeclaredField()等方法,可以获取类中的方法和字段的信息。getMethod()和getField()方法可以获取类及其父类中的公共方法和字段,而getDeclaredMethod()和getDeclaredField()方法可以获取类中声明的所有方法和字段,包括私有的。
调用方法和访问字段:通过Method对象的invoke()方法,可以调用方法;通过Field对象的get()和set()方法,可以获取和设置字段的值。需要注意的是,对于私有方法和字段,需要先通过setAccessible(true)方法设置为可访问。
反射的优点是在运行时动态地获取和操作类的信息,使得程序更加灵活和可扩展。它在某些情况下非常有用,例如在框架和库中使用反射来实现插件化、依赖注入等功能。但反射也有一些缺点,包括性能较差(反射操作通常比直接调用方法和访问字段更慢)、编译时类型检查的缺失以及代码可读性降低等。
需要注意的是,在使用反射时应当谨慎,确保对于安全性要求较高的场景进行适当的权限控制和错误处理,以避免潜在的安全问题和运行时异常。
多线程和线程池
设计模式(六大基本原则、项目中常用的设计模式、手写单例等)
生产者模式和消费者模式的区别?
在设计模式中,生产者模式和消费者模式指的是两种不同的设计模式,它们的主要区别如下:
目的和重点:
生产者模式(Producer Pattern):也称为发布-订阅(Publish-Subscribe)模式,重点在于解耦生产者和消费者之间的关系。生产者负责生成消息或事件,并将其发布到一个或多个订阅者,订阅者可以选择订阅自己感兴趣的消息或事件。
消费者模式(Consumer Pattern):重点在于消费者如何有效地获取并处理数据或事件。消费者从一个或多个生产者那里获取数据,并进行相应的消费处理。
关注的对象:
生产者模式:主要关注生产者和订阅者之间的解耦,通过引入一个消息中心或事件总线来实现生产者和消费者的解耦。
消费者模式:主要关注消费者对数据或事件的获取和处理,消费者需要主动从生产者那里获取数据。
数据流向:
生产者模式:数据流向是从生产者到订阅者,生产者产生消息或事件,并将其发布到消息中心或事件总线,然后订阅者从中心或总线订阅并接收消息。
消费者模式:数据流向是从生产者到消费者,生产者生成数据或事件,并将其提供给消费者进行处理。
解耦方式:
生产者模式:通过引入消息中心或事件总线,生产者和消费者之间的关系得到解耦,生产者只需将消息发布到中心,而不需要直接与消费者进行交互。
消费者模式:生产者和消费者之间的解耦方式较少关注,可以直接进行交互,消费者主动从生产者那里获取数据。
总之,生产者模式和消费者模式在设计模式中是两种不同的概念,重点和关注点有所不同。生产者模式关注生产者和消费者之间的解耦合,通过消息中心或事件总线来实现;而消费者模式关注消费者对数据或事件的获取和处理。
单例模式双重加锁,为什么这样做?
在单例模式的实现中,双重加锁(Double-Checked Locking)是一种常见的技术手段,用于在多线程环境下实现延迟初始化的单例对象,同时保证线程安全性。双重加锁的主要目的是在保证性能的同时,避免不必要的同步开销。
下面是双重加锁的实现代码示例:
java
复制
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
// 私有构造函数
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
双重加锁的原理如下:
第一次检查(非同步块内):首先检查实例变量instance是否为空,如果为空,则进入同步块。
同步块内检查(第二次检查):在同步块内再次检查instance是否为空。由于同步块只能由一个线程执行,因此只有一个线程可以进入同步块。如果instance仍然为空,则在同步块内创建单例对象。
返回实例:无论是在同步块内创建了新的实例,还是在同步块外已经存在了实例,最终都会返回单例对象。
双重加锁的原因是为了在多线程环境下保证线程安全性和性能:
线程安全性:双重加锁利用了同步块来确保只有一个线程能够创建实例对象。在第一次检查时,如果实例为空,进入同步块后再次检查实例是否为空,避免了多个线程同时进入同步块创建实例的情况。
性能优化:在多线程环境下,如果没有双重加锁的机制,所有线程都需要进入同步块来检查实例是否为空,这会带来额外的同步开销。通过双重加锁,只有在实例为空时才需要进入同步块,避免了大部分情况下的同步操作,提高了性能。
需要注意的是,双重加锁需要将实例变量声明为volatile,以确保对实例的可见性。此外,在某些特定的情况下,双重加锁可能存在一些问题,例如在早期的 Java 版本中可能出现的指令重排序问题。因此,在使用双重加锁时,需要对具体的编程语言和环境进行适当的考虑和测试,以确保其正确性和可靠性。
知道的设计模式有哪些?
项目中常用的设计模式有哪些?
手写生产者、消费者模式。
以下是使用Java实现的简单生产者-消费者模式示例:
java
复制
import java.util.LinkedList;
class Producer implements Runnable {
private LinkedList buffer;
private int maxSize;
public Producer(LinkedList buffer, int maxSize) {
this.buffer = buffer;
this.maxSize = maxSize;
}
@Override
public void run() {
int counter = 1;
while (true) {
try {
produce(counter++);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void produce(int item) throws InterruptedException {
synchronized (buffer) {
while (buffer.size() == maxSize) {
System.out.println("Buffer is full. Producer is waiting...");
buffer.wait();
}
buffer.add(item);
System.out.println("Producer produced item: " + item);
buffer.notifyAll();
}
}
}
class Consumer implements Runnable {
private LinkedList buffer;
public Consumer(LinkedList buffer) {
this.buffer = buffer;
}
@Override
public void run() {
while (true) {
try {
consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void consume() throws InterruptedException {
synchronized (buffer) {
while (buffer.isEmpty()) {
System.out.println("Buffer is empty. Consumer is waiting...");
buffer.wait();
}
int item = buffer.removeFirst();
System.out.println("Consumer consumed item: " + item);
buffer.notifyAll();
}
}
}
public class ProducerConsumerExample {
public static void main(String[] args) {
LinkedList buffer = new LinkedList<>();
int maxSize = 5;
Producer producer = new Producer(buffer, maxSize);
Consumer consumer = new Consumer(buffer);
Thread producerThread = new Thread(producer);
Thread consumerThread = new Thread(consumer);
producerThread.start();
consumerThread.start();
}
}
在这个示例中,Producer类代表生产者,Consumer类代表消费者。它们都实现了Runnable接口,通过run()方法定义了各自的行为。
在Producer类中,通过synchronized关键字锁定了buffer对象,确保在生产时只有一个线程能够访问buffer。当buffer已满时,生产者调用buffer.wait()进行等待,直到有消费者从buffer中取走一个元素后,它会被唤醒并继续生产。
在Consumer类中,同样通过synchronized关键字锁定了buffer对象,确保在消费时只有一个线程能够访问buffer。当buffer为空时,消费者调用buffer.wait()进行等待,直到有生产者向buffer中添加一个元素后,它会被唤醒并继续消费。
在主函数中,创建了一个buffer作为生产者和消费者之间的共享缓冲区。然后创建了一个生产者线程和一个消费者线程,并启动它们。
当程序运行时,生产者会不断地生成自增的项目,并将其添加到缓冲区中,而消费者会从缓冲区中取出项目并消费。生产者和消费者之间通过wait()和notifyAll()方法进行协调,以确保在正确的时间进行生产和消费,避免了竞态条件和线程安全问题。
手写观察者模式代码。
下面是一个简单的Java代码示例,演示了观察者模式的实现:
java
复制
import java.util.ArrayList;
import java.util.List;
// 主题接口
interface Subject {
void registerObserver(Observer observer);
void unregisterObserver(Observer observer);
void notifyObservers(String message);
}
// 具体主题类
class ConcreteSubject implements Subject {
private List observers = new ArrayList<>();
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}
@Override
public void unregisterObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers(String message) {
for (Observer observer : observers) {
observer.update(message);
}
}
}
// 观察者接口
interface Observer {
void update(String message);
}
// 具体观察者类
class ConcreteObserver implements Observer {
private String name;
public ConcreteObserver(String name) {
this.name = name;
}
@Override
public void update(String message) {
System.out.println(name + " received message: " + message);
}
}
public class ObserverPatternExample {
public static void main(String[] args) {
ConcreteSubject subject = new ConcreteSubject();
ConcreteObserver observer1 = new ConcreteObserver("Observer 1");
ConcreteObserver observer2 = new ConcreteObserver("Observer 2");
ConcreteObserver observer3 = new ConcreteObserver("Observer 3");
subject.registerObserver(observer1);
subject.registerObserver(observer2);
subject.registerObserver(observer3);
subject.notifyObservers("Hello, observers!");
subject.unregisterObserver(observer2);
subject.notifyObservers("Goodbye, observer 2!");
}
}
在这个示例中,我们定义了一个Subject接口和一个ConcreteSubject类作为主题(被观察者),定义了一个Observer接口和一个ConcreteObserver类作为观察者。
Subject接口定义了注册观察者、注销观察者和通知观察者的方法。ConcreteSubject类实现了Subject接口,并维护了一个观察者列表,实现了注册、注销和通知的逻辑。
Observer接口定义了观察者的更新方法。ConcreteObserver类实现了Observer接口,并在update方法中输出接收到的消息。
在主函数中,我们创建了一个具体主题对象(ConcreteSubject),以及三个具体观察者对象(ConcreteObserver)。然后,我们通过registerObserver方法将观察者注册到主题中。接下来,我们使用notifyObservers方法通知所有观察者,传递一条消息。观察者们收到消息后会进行相应的处理。最后,我们使用unregisterObserver方法注销了一个观察者,并再次通知观察者们。
运行该代码,观察者们将接收到相应的消息并进行输出。观察者模式实现了一种松耦合的方式,使得主题和观察者之间的关系解耦,可以方便地添加、删除和通知观察者,以实现事件的触发和处理。
适配器模式、装饰者模式、外观模式的异同?
适配器模式、装饰者模式和外观模式是常见的设计模式,它们都属于结构型模式,但在解决的问题和使用方式上有一些区别。
适配器模式(Adapter Pattern)的目的是将一个类的接口转换成客户端所期望的另一个接口,从而使得原本不兼容的类能够一起工作。适配器模式通过创建一个适配器类来实现接口的转换,使得客户端可以统一调用适配器的接口,而适配器内部则负责进行与被适配对象的交互。适配器模式主要解决接口不兼容的问题。
装饰者模式(Decorator Pattern)的目的是动态地给一个对象添加额外的功能,而无需修改其原始类。装饰者模式通过创建装饰者类,该类实现了与原始类相同的接口,并持有一个原始类的实例,通过装饰者类的包装,可以在不改变原始类的情况下,动态地添加新的行为或修改原始类的行为。
外观模式(Facade Pattern)的目的是为了提供一个简化的接口,隐藏系统复杂性,让客户端能够更方便地使用系统。外观模式通过创建一个外观类,该类提供了一个高层次的接口,将客户端的请求委派给系统内部的各个子系统进行处理。外观模式主要解决系统复杂性带来的使用困难。
异同点总结如下:
目的:
适配器模式:解决接口之间的不兼容问题。
装饰者模式:动态地给对象添加额外的功能。
外观模式:提供一个简化的接口,隐藏系统的复杂性。
使用方式:
适配器模式:通过创建适配器类,将一个接口转换成另一个接口。
装饰者模式:通过创建装饰者类,包装原始对象,添加新的行为。
外观模式:通过创建外观类,提供一个简化的接口,将客户端的请求委派给内部的子系统。
关注点:
适配器模式:关注接口的转换和兼容性。
装饰者模式:关注给对象添加新的行为或修改行为。
外观模式:关注隐藏系统的复杂性,提供简化的接口。
虽然适配器模式、装饰者模式和外观模式具有不同的目的和使用方式,但它们都可以用于改善代码的灵活性、可维护性和可扩展性。具体选择哪种模式取决于应用场景和需求。
谈谈对 java 状态机的理解。
Java状态机(Java State Machine)是一种表示对象状态和状态转换的编程模型。它将对象的行为和状态抽象为有限的状态集合,并定义了在不同状态之间转换时的行为。
在Java中,状态机可以通过以下几个主要元素来实现:
状态(State):状态是对象所处的特定情况或条件。它可以是一个枚举类或类的实例,代表对象的不同状态。
事件(Event):事件是触发状态转换的动作或条件。它可以是方法调用、用户操作、消息等。
状态转换(Transition):状态转换定义了从一个状态到另一个状态的规则。它通常与特定的事件相关联,并指定了在触发事件时应该执行的操作。
动作(Action):动作是在状态转换过程中执行的操作或行为。它可以是方法调用、数据更新、状态变更等。
Java状态机的实现方式可以有多种,例如使用if-else语句、switch语句、表格驱动等。一种常见的方式是使用状态模式(State Pattern),其中每个状态都被封装为一个具体类,而状态机本身由一个上下文对象来管理和执行状态转换。
使用Java状态机的好处包括:
可读性和可维护性:状态机模型能够清晰地表达对象的状态和状态转换规则,使代码易于理解和维护。
灵活性和扩展性:通过定义不同的状态和状态转换,可以灵活地定义对象的行为,并在需要时方便地添加新的状态和处理逻辑。
可测试性:状态机的状态和状态转换规则是可测试的,可以针对不同的状态和事件编写针对性的测试用例。
Java状态机适用于许多场景,例如游戏开发、工作流管理、网络协议处理等。它提供了一种结构化的方式来组织和管理对象的行为,使得代码更加清晰、可扩展和可维护。
谈谈应用更新(灰度、强制更新、分区更新?)
当涉及到Android应用更新时,有几种常见的策略和技术可以使用。下面我将谈论灰度发布、强制更新和分区更新这三种常见的应用更新方式。
灰度发布(Gray/Canary Release):灰度发布是一种逐步将更新推送给一小部分用户的策略。在灰度发布中,应用更新不是立即对所有用户可用,而是先对一小部分用户进行测试和评估。这样可以降低潜在问题对所有用户的影响,并在需要时及时进行修复。灰度发布通常涉及使用特定的工具或服务来管理和控制更新的分发,并根据用户反馈和性能指标进行动态调整。
强制更新(Force Update):强制更新是一种要求所有用户立即安装应用更新的策略。在强制更新中,应用程序检查是否有新版本可用,并在用户打开应用时显示一个强制更新的提示。如果用户不安装更新,他们将无法继续使用旧版本的应用。强制更新通常用于确保所有用户都使用最新的应用版本,以提供新功能、修复漏洞或满足法律要求等。
分区更新(Staged Rollout):分区更新是一种逐步将更新推送给用户群体的策略。与灰度发布不同,分区更新将用户划分为多个不同的群体,每个群体在不同的时间段内接收更新。这种方法可以帮助开发人员逐步扩展更新的范围,并在每个阶段中观察和处理任何问题。分区更新通常使用应用商店或发布平台提供的功能来管理和控制更新的分发。
这些应用更新策略可以根据具体的需求和应用情况来选择和组合使用。灰度发布和分区更新可以帮助开发人员控制更新的范围和速度,以最小化潜在问题的影响。强制更新则可以确保用户始终使用最新版本的应用,以提供最佳的用户体验和安全性。
需要注意的是,在实施这些策略时,开发人员还应考虑用户体验、数据安全性、网络带宽和设备兼容性等因素,以确保应用更新的成功和有效性。
断点续传
在Android中实现断点续传功能,可以按照以下步骤进行操作:
检查是否支持断点续传:在服务器端,确保文件下载接口支持断点续传。可以通过检查HTTP响应头中是否包含"Accept-Ranges"字段来验证服务器是否支持断点续传。
分块下载:将大文件分成多个较小的块进行下载。每个块的大小可以根据需求进行调整。一种常见的做法是使用HTTP Range头来请求指定范围的数据块。通过设置Range头,服务器可以返回指定范围的文件数据。
保存已下载的数据块:在下载过程中,将已下载的数据块保存到本地存储设备上。可以使用临时文件或特定的文件命名规则来保存每个数据块。这样,在断线重连或应用关闭后,可以继续下载时恢复下载进度。
下载进度管理:在UI界面上展示下载进度,让用户了解当前的下载状态。可以使用回调机制或广播来更新进度条或显示进度百分比。
错误处理和恢复:处理下载过程中的错误和异常情况。例如,网络连接断开、服务器错误、磁盘空间不足等。在这些情况下,需要相应地处理错误并提供恢复机制,以便能够在重新连接时继续下载。
恢复下载:当应用重新启动或网络重新连接时,检查之前的下载状态和已下载的数据块。根据已下载的数据块信息,发送带有Range头的请求以继续下载未完成的部分。
下载完成和合并:在所有数据块都下载完成后,将它们合并成最终的完整文件。可以根据需要使用文件合并的库或自定义逻辑来实现文件合并。
通过以上步骤,可以实现Android应用中的断点续传功能,使用户能够在下载大文件时具备中断后继续下载的能力。
Java 四大引用
在Java中,有四种主要的引用类型,也被称为Java四大引用。它们是:
强引用(Strong Reference):强引用是最常见的引用类型。当我们使用new关键字创建一个对象并将其赋值给一个引用变量时,这个引用变量就是一个强引用。在强引用存在时,垃圾回收器不会回收被引用的对象。只有当没有任何强引用指向一个对象时,垃圾回收器才会将其回收。
软引用(Soft Reference):软引用是一种比强引用弱一些的引用类型。当内存不足时,垃圾回收器会尝试回收只有软引用指向的对象。软引用可以用于实现缓存等功能,当内存不足时,可以选择性地回收一些缓存对象,而不是导致OutOfMemoryError。可以使用SoftReference类来创建软引用。
弱引用(Weak Reference):弱引用的生命周期更短暂。即使有弱引用指向一个对象,只要垃圾回收器运行,就会立即回收被弱引用指向的对象。弱引用通常用于解决内存泄漏问题或短暂持有对象的情况。可以使用WeakReference类来创建弱引用。
虚引用(Phantom Reference):虚引用是最弱的引用类型。虚引用几乎没有实际的应用场景,主要用于管理直接内存。与其他引用类型不同,虚引用必须与引用队列(ReferenceQueue)一起使用。当垃圾回收器决定回收一个对象时,会将虚引用添加到引用队列中,以通知应用程序对象的回收情况。可以使用PhantomReference类来创建虚引用。
使用不同类型的引用可以提供更灵活和精细的对象管理。在特定的场景中,选择适当的引用类型可以帮助我们优化内存使用和解决一些潜在的问题,如内存泄漏等。
Java 的泛型、泛型擦除、通配符相关的东西
在Java中,泛型是一种强类型检查机制,允许我们在编译时指定操作的数据类型。它提供了类型安全性和代码重用的好处。下面是泛型、泛型擦除和通配符的区别:
泛型(Generics):泛型是Java中引入的一种参数化类型的概念。使用泛型,我们可以在类、接口、方法中定义和使用参数化类型,以增加代码的灵活性和可重用性。通过泛型,可以在编译时进行类型检查,避免在运行时出现类型转换错误。
泛型擦除(Type Erasure):泛型擦除是Java泛型的实现方式。在编译后,Java的泛型信息会被擦除,即将泛型类型的信息移除,转而使用原始类型。这是因为Java的泛型是在编译时进行类型检查的,而在运行时并不保留泛型的具体类型信息。泛型擦除的结果是,泛型类型参数被替换为其边界或Object类型,并进行必要的类型转换。
通配符(Wildcard):通配符是用来表示未知类型的符号,通常使用问号(?)表示。通配符可以用在泛型类型的声明中,用于增加灵活性。通配符分为上界通配符和无界通配符。上界通配符使用extends关键字限定类型的上界,表示只能是某个类型或其子类型;无界通配符使用问号(?)表示,表示可以是任意类型。
区别总结如下:
泛型是一种参数化类型的概念,用于在编译时指定操作的数据类型。
泛型擦除是Java泛型的实现方式,在编译后将泛型信息擦除,并使用原始类型进行替换。
通配符是用于表示未知类型的符号,可以增加泛型类型的灵活性。通配符分为上界通配符和无界通配符,用于限定类型的范围。
需要注意的是,由于泛型擦除的存在,运行时无法获取泛型的具体类型信息。因此,在使用泛型时需要谨慎处理类型转换和泛型边界等问题,以确保类型安全性和正确性。
final、finally、finalize 的区别
在Java中,final、finally和finalize是三个不同的关键字,它们有不同的用途和行为。下面是它们的区别:
final:final是一个修饰符,可应用于类、方法和变量。使用final修饰的类表示该类不能被继承,使用final修饰的方法表示该方法不能被子类重写,使用final修饰的变量表示该变量是一个常量,一旦赋值后就不能再改变。final关键字提供了不可变性、安全性和性能优化等方面的好处。
finally:finally是一个关键字,用于定义在异常处理语句中的一个代码块。无论是否发生异常,finally块中的代码总是会被执行。通常在finally块中执行一些清理资源的操作,例如关闭文件、释放数据库连接等。finally块通常与try-catch语句结合使用,以确保资源的正确释放。
finalize:finalize是一个方法,定义在Object类中,可被子类重写。它是垃圾回收器在回收对象之前调用的方法。垃圾回收器在确定对象不再被引用时,会调用该对象的finalize方法来进行资源回收和清理操作。然而,Java中不鼓励使用finalize方法,因为它的调用时机不确定且不可靠。相反,应该使用显式资源释放的方式,例如在finally块中手动关闭资源。
总结:
final是一个修饰符,用于表示不可更改的类、方法或变量。
finally是一个关键字,用于定义在异常处理语句中必须执行的代码块。
finalize是一个方法,用于进行垃圾回收前的资源清理操作,但在实际开发中不应过度依赖它,而应使用显式资源释放的方式。
接口、抽象类的区别
接口(Interface)和抽象类(Abstract Class)是Java中用于实现抽象和多态的两个重要概念,它们有以下区别:
定义方式:接口使用interface关键字定义,抽象类使用abstract关键字定义。
继承关系:一个类可以实现多个接口,但只能继承一个抽象类。类实现接口时使用implements关键字,类继承抽象类时使用extends关键字。
构造函数:接口不能包含构造函数,因为接口是抽象的,无法实例化。抽象类可以包含构造函数,用于子类的初始化。
方法实现:接口中的方法都是抽象的,没有方法体,子类实现接口时必须提供方法的具体实现。抽象类可以包含抽象方法和具体方法,子类可以选择性地实现抽象方法或继承具体方法。
变量:接口中可以定义常量,且默认为public static final类型。抽象类中可以定义普通变量,且可以有不同的访问修饰符。
多态性:接口更灵活,一个类可以实现多个接口,从而实现多态性。抽象类的多态性相对较少,一个类只能继承一个抽象类。
设计目的:接口主要用于定义行为规范,描述类应该具有的方法。抽象类主要用于被继承,提供一部分通用实现,并定义抽象方法供子类实现。
总体而言,接口适用于定义纯粹的契约,强调类的行为规范和多态性。抽象类适用于具有某些通用实现的类,其中可能包含一些具体方法和抽象方法。选择接口还是抽象类取决于具体的需求和设计目标。
二、计算机网络部分:
TCP/IP原理,TCP 有哪些状态
TCP/IP(Transmission Control Protocol/Internet Protocol)是一组网络协议,用于在互联网上进行数据通信。TCP/IP协议族包含多个协议,其中最重要的是TCP协议(传输控制协议)。下面是TCP的一些重要状态:
CLOSED(关闭):表示TCP连接处于关闭状态,没有建立或已经关闭。
LISTEN(监听):表示TCP服务器正在等待传入连接请求,准备接受客户端的连接。
SYN_SENT(同步已发送):表示客户端已发送连接请求(SYN)到服务器,等待服务器的确认。
SYN_RECEIVED(同步已接收):表示服务器已接收到客户端的连接请求,并发送确认请求(SYN+ACK)给客户端。
ESTABLISHED(已建立):表示TCP连接已成功建立,双方可以进行数据传输。
FIN_WAIT_1(终止等待1):表示TCP连接中的一方(通常是客户端)已发送连接终止请求(FIN),等待另一方的确认。
FIN_WAIT_2(终止等待2):表示TCP连接中的一方仍然等待另一方发送连接终止请求(FIN)。
CLOSE_WAIT(关闭等待):表示TCP连接中的一方已收到连接终止请求(FIN),并发送确认,等待关闭。
LAST_ACK(最后确认):表示TCP连接中的一方(通常是服务器)已发送最后的连接终止请求(FIN),等待对方的确认。
TIME_WAIT(时间等待):表示TCP连接中的一方已发送连接终止请求(FIN),并接收到对方的确认,但仍然保持连接等待一段时间,以确保对方的确认已到达。
CLOSED(关闭):表示TCP连接已完全关闭,双方都确认连接终止。
这些是TCP连接中的一些常见状态,用于描述TCP连接的不同阶段和状态变化。注意,具体的实现可能会有一些额外的状态或细微的差别,但上述状态是TCP连接过程中的常见情况。
三次握手、四次挥手。为啥不是三次不是两次
TCP使用三次握手建立连接和四次挥手关闭连接的原因如下:
三次握手建立连接:
第一次握手:客户端发送一个带有SYN(同步序列编号)标志的TCP报文段,请求建立连接。
第二次握手:服务器收到客户端的请求后,回复一个带有SYN和ACK(确认序列编号)标志的TCP报文段,表示同意建立连接。
第三次握手:客户端收到服务器的回复后,再次发送一个带有ACK标志的TCP报文段,确认连接建立。
这种三次握手的过程是为了确保双方都能正确地收发数据,建立可靠的连接。如果只有两次握手,可能会出现以下情况:
客户端发送连接请求,但由于网络延迟或其他原因,服务器没有收到请求。这样,如果只有两次握手,服务器无法确认客户端的请求,也就无法建立连接。
客户端发送连接请求后,服务器回复了确认,但这个确认在传输过程中丢失了。这样,如果只有两次握手,客户端会一直等待服务器的响应,建立不了连接。
通过三次握手,可以确保双方都能正确收发数据,避免了上述问题。
四次挥手关闭连接:
第一次挥手:当客户端希望关闭连接时,发送一个带有FIN(结束)标志的TCP报文段,表示不再发送数据。
第二次挥手:服务器收到客户端的关闭请求后,发送一个带有ACK标志的TCP报文段,确认接收到客户端的关闭请求。
第三次挥手:服务器发送一个带有FIN标志的TCP报文段,表示服务器不再发送数据。
第四次挥手:客户端收到服务器的关闭请求后,发送一个带有ACK标志的TCP报文段,确认关闭请求。
四次挥手的过程是为了确保双方都能正确地关闭连接,完成所有数据的传输和确认。如果只有三次挥手,可能会导致以下问题:
客户端发送关闭请求后,服务器收到后立即关闭连接。但服务器可能还有一些未传输完的数据,这些数据会丢失。
客户端发送关闭请求后,服务器回复确认后立即关闭连接。但客户端可能还有一些未传输完的数据,这些数据会丢失。
通过四次挥手,可以确保双方都能完成数据传输和确认,避免了数据丢失和连接未完全关闭的问题。
HTTPS 和 HTTP 的区别
HTTPS(HyperText Transfer Protocol Secure)和HTTP(HyperText Transfer Protocol)是两种用于在Web上进行数据通信的协议,它们在以下方面有所区别:
安全性:
HTTP是明文传输协议,数据在传输过程中不加密,容易被窃听和篡改。
HTTPS使用SSL/TLS协议对数据进行加密和身份验证,确保数据在传输过程中的机密性和完整性,提供更高的安全性。
端口:
HTTP默认使用端口80进行通信。
HTTPS默认使用端口443进行通信。
证书:
HTTP不需要证书,任何人都可以发送HTTP请求。
HTTPS使用数字证书,由受信任的证书颁发机构(CA)签发,用于证明网站的身份和加密通信。
URL前缀:
HTTP的URL以"http://"开头。
HTTPS的URL以"https://"开头。
运行机制:
HTTP在应用层传输数据,使用TCP作为传输协议。
HTTPS在HTTP的基础上加入了SSL/TLS协议,在应用层和传输层之间增加了安全层,用于加密和解密数据。
总结:
HTTPS是HTTP的安全版本,使用SSL/TLS协议对数据进行加密和身份验证,提供更高的安全性。相比之下,HTTP是明文传输协议,数据在传输过程中不受保护。因此,当涉及敏感信息(如登录凭据、支付信息等)时,使用HTTPS更为安全,以确保数据的机密性和完整性。
HTTP 2.0 3.0的区别
目前(截至2021年9月,知识截断点),HTTP/2和HTTP/3是HTTP协议的新版本,它们与HTTPS(HTTP over SSL/TLS)协议结合使用。下面是HTTP/2和HTTP/3的一些区别:
传输方式:
HTTP/2使用二进制传输,将HTTP消息拆分为更小的帧,并在客户端和服务器之间进行多路复用传输,从而提高效率。
HTTP/3使用QUIC(Quick UDP Internet Connections)作为传输协议,而不是TCP。QUIC基于UDP,具有更低的延迟和更好的拥塞控制,适用于不稳定的网络环境。
连接建立:
HTTP/2使用多路复用技术,允许在单个连接上同时发送多个请求和响应,避免了建立多个TCP连接的开销。
HTTP/3也使用多路复用,并且在QUIC协议的基础上进一步改进了连接建立和管理的性能。
传输效率:
HTTP/2引入了头部压缩技术(HPACK)来减少重复的头部信息,减小了数据传输的大小。
HTTP/3在QUIC协议的基础上具有更好的拥塞控制、流量控制和错误恢复机制,提供了更高的传输效率和性能。
安全性:
HTTP/2和HTTP/3都可以与HTTPS协议结合使用,通过SSL/TLS加密数据传输,确保通信的安全性。
需要注意的是,HTTP/2和HTTP/3是HTTP协议的升级版本,用于改进性能和效率,并与HTTPS协议结合使用。它们的具体特性和实现可能会随着时间的推移而有所变化
Socket通信
Socket通信是一种在计算机网络中进行进程间通信的方式。它基于套接字(socket)的概念,通过网络连接使得不同计算机上的进程能够进行数据的传输和交流。
Socket通信的原理可以概括为以下几个步骤:
创建Socket:首先,客户端和服务器分别创建自己的Socket对象。在大多数编程语言中,可以使用Socket库或API来创建和操作Socket对象。
建立连接:客户端通过Socket对象向服务器发起连接请求。服务器监听特定的端口,当收到连接请求时,创建一个新的Socket对象来与客户端进行通信。
数据传输:一旦连接建立,客户端和服务器之间可以通过各自的Socket对象进行数据的发送和接收。数据可以被分割成小的数据包(也称为数据报)来进行传输。
断开连接:当客户端或服务器需要关闭连接时,它们可以通过Socket对象发送关闭请求。对方将接收到这个请求,并进行相应的处理,最终关闭连接。
在Socket通信中,常见的通信模式有两种:
TCP(Transmission Control Protocol):TCP提供可靠的、面向连接的通信。它使用三次握手建立连接,通过字节流的方式传输数据,确保数据的可靠性和顺序性。TCP适用于需要可靠传输的场景,如文件传输、网页访问等。
UDP(User Datagram Protocol):UDP是一种无连接的通信协议。它以数据报的形式发送数据,不保证数据的可靠性和顺序性。UDP适用于实时性要求较高的场景,如音视频传输、实时游戏等。
Socket通信是基于网络协议的一种通信方式,它提供了一种简单而灵活的方式,使得不同计算机上的进程能够进行数据的传输和交流。
浏览器输入一个 URL 按下回车网络传输的流程?
当您在浏览器中输入一个URL并按下回车键时,以下是网络传输的基本流程:
URL解析:浏览器首先解析输入的URL,提取出协议(例如HTTP或HTTPS)、主机名(例如www.example.com)和路径等信息。
DNS解析:浏览器将主机名发送给本地DNS服务器,以获取对应主机的IP地址。如果DNS缓存中没有相应的记录,DNS服务器将递归地查询其他DNS服务器,直至找到与主机名对应的IP地址。
TCP连接的建立:浏览器使用HTTP协议与服务器建立TCP连接。这涉及到三次握手的过程,即客户端向服务器发送连接请求(SYN),服务器回复确认和连接请求(SYN+ACK),最后客户端回复确认(ACK)。
发送HTTP请求:一旦TCP连接建立,浏览器会构建一个HTTP请求报文,包括请求方法(GET、POST等)、路径、请求头部信息等。然后将该请求发送到服务器。
服务器处理请求:服务器接收到浏览器发送的HTTP请求后,根据请求的路径和其他相关信息,处理请求并生成响应。
接收HTTP响应:服务器生成HTTP响应报文,包括状态码(如200表示成功)、响应头部信息和响应体(包含请求的数据)。然后将响应发送回浏览器。
渲染页面:浏览器接收到HTTP响应后,会解析响应报文,根据响应头部信息确定内容的类型(如HTML、CSS、JavaScript等),然后开始渲染页面。它会处理HTML标记、加载和解析CSS样式表、执行JavaScript代码等,最终将页面呈现给用户。
关闭TCP连接:一旦页面加载完成,浏览器会关闭与服务器之间的TCP连接,释放资源。
这是一个简化的描述,实际过程中还涉及数据分段、路由选择、数据传输的可靠性保证等其他细节。但以上流程概括了从浏览器输入URL到页面加载完成的基本网络传输过程。
网络架构,每层有什么协议
计算机网络通常采用分层的架构,以便在不同的层次上实现功能模块化和协议的划分。以下是常见的网络架构模型和每个层次的一些重要协议:
OSI参考模型(Open Systems Interconnection Reference Model):
物理层:负责传输比特流,将数据转换为电信号进行物理传输。常见协议:Ethernet、FDDI、RS-232等。
数据链路层:提供可靠的点对点数据传输。常见协议:Ethernet(IEEE 802.3)、PPP(Point-to-Point Protocol)、HDLC(High-Level Data Link Control)等。
网络层:负责数据包的路由和转发,实现不同网络之间的通信。常见协议:IP(Internet Protocol)、ICMP(Internet Control Message Protocol)、ARP(Address Resolution Protocol)等。
传输层:提供端到端的可靠数据传输和错误恢复。常见协议:TCP(Transmission Control Protocol)、UDP(User Datagram Protocol)等。
会话层:处理应用程序之间的会话控制和同步。常见协议:SSH(Secure Shell)、RPC(Remote Procedure Call)等。
表示层:处理数据的编码、解码和加密解密等。常见协议:TLS(Transport Layer Security)、ASCII(American Standard Code for Information Interchange)等。
应用层:提供特定应用程序的服务和协议。常见协议:HTTP(Hypertext Transfer Protocol)、FTP(File Transfer Protocol)、SMTP(Simple Mail Transfer Protocol)等。
TCP/IP模型(Transmission Control Protocol/Internet Protocol):
网络接口层(或链路层):负责物理连接和数据链路通信。常见协议:Ethernet、ARP等。
网际层(或网络层):负责数据包的路由和转发。常见协议:IP、ICMP等。
传输层:提供端到端的可靠数据传输和错误恢复。常见协议:TCP、UDP等。
应用层:提供特定应用程序的服务和协议。常见协议:HTTP、FTP、SMTP等。
需要注意的是,这里列举的协议只是举例,实际上还有许多其他协议在不同层次的网络架构中使用。此外,随着网络技术的不断发展,新的协议和标准也在不断涌现。
FTP 相关原理
FTP(File Transfer Protocol)是一种用于在网络上进行文件传输的协议。以下是FTP的一般工作原理:
建立连接:客户端使用TCP连接与FTP服务器建立连接。默认情况下,FTP使用端口号21进行控制连接。
身份验证:客户端向服务器发送用户名和密码进行身份验证。一旦验证成功,客户端就可以执行后续的文件传输操作。
控制连接:通过控制连接,客户端与服务器之间进行命令和响应的传输。客户端发送不同的FTP命令(如LIST、GET、PUT等)来请求服务器执行相应的操作。
数据连接:FTP在数据传输时使用不同的数据连接。数据连接可以是主动模式或被动模式。
主动模式:在主动模式下,客户端在端口号20上监听数据连接,服务器使用新的端口号连接到客户端的端口号20,进行数据的传输。
被动模式:在被动模式下,服务器在一个指定的端口号上监听数据连接,客户端通过控制连接发送PASV命令获取服务器的IP地址和端口号,然后在该端口上建立数据连接进行传输。
文件传输:一旦数据连接建立,文件传输可以开始。根据客户端发送的命令,服务器将相应的文件发送给客户端(GET命令)或接收客户端发送的文件(PUT命令)。
断开连接:文件传输完成后,客户端可以通过发送QUIT命令来关闭控制连接,终止与服务器的FTP会话。
需要注意的是,FTP使用明文传输,不提供加密功能,因此在安全性方面存在一些风险。为了安全传输文件,可以使用FTP over SSL/TLS(FTPS)或SSH File Transfer Protocol(SFTP),它们分别在FTP协议上加入了SSL/TLS或SSH协议的加密和身份验证机制。
此外,还有一些高级功能和命令可供使用,如目录导航、文件重命名、权限设置等。FTP的具体实现和功能可能会因不同的服务器和客户端而有所差异,但以上概述了FTP的一般工作原理。
TCP 建立连接后,发包频率是怎么样的?
TCP在建立连接后,发送数据的频率是由拥塞控制算法来调节的。拥塞控制算法旨在确保网络的稳定性和公平性,避免过度拥塞和网络拥塞的发生。
1.慢启动:
在TCP的初始阶段,发送方会采用慢启动算法,初始发送窗口大小比较小。发送方发送的数据包数量逐渐增加,以探测网络的可用带宽。发送方每收到一个确认(ACK)就会增加发送窗口的大小,从而增加发送数据的速率。
2.拥塞避免:
随着时间的推移,发送方逐渐进入拥塞避免阶段。在拥塞避免阶段,发送方以一种更加保守的方式增加发送窗口的大小,以避免网络拥塞。发送方会周期性地发送数据包,在收到对应的ACK之后增加发送窗口的大小。
- 拥塞控制:快重传/快恢复
如果网络拥塞发生,发送方会根据网络的反馈进行拥塞控制。发送方会减小发送窗口的大小,从而降低发送数据的速率。拥塞控制算法包括慢启动、拥塞避免和快重传/快恢复等机制,以便在网络拥塞时进行适当的调整。
因此,TCP的发包频率是由拥塞控制算法动态调节的,根据网络的状况和拥塞程度来决定发送数据的速率。这样可以确保TCP连接能够在网络上稳定地传输数据,避免过度拥塞和网络拥塞的发生。