网易上 面试
分析 Error 和 Exception 的区别
在 Java 编程中,Error 和 Exception 都继承自 Throwable 类,它们用于处理程序运行时出现的异常情况,但两者存在显著区别。
Error 通常表示系统级别的错误,是 Java 虚拟机(JVM)无法处理的严重问题,比如 OutOfMemoryError (内存溢出错误),当应用程序试图分配的内存超过 JVM 可用内存时会抛出此错误;还有 StackOverflowError (栈溢出错误),多发生在递归调用没有正确终止条件时,导致栈空间耗尽。这类错误一般不是由程序代码的逻辑错误直接导致,而是由于外部环境问题或系统资源耗尽等原因引发,应用程序通常无法恢复或处理这些错误,一旦发生往往会导致 JVM 崩溃。
Exception 则表示程序运行时发生的、可以被捕获和处理的异常情况,它分为受检异常(Checked Exception)和非受检异常(Unchecked Exception)。受检异常要求在编译时进行处理,比如 IOException ,当读取文件时文件不存在或者网络连接出现问题时会抛出此异常,程序必须通过 try - catch 块捕获或者使用 throws 关键字声明抛出,否则无法通过编译。非受检异常不需要在编译时显式处理,它们通常是由于编程错误导致,比如 NullPointerException (空指针异常),当试图访问一个空对象的方法或属性时会抛出;ArrayIndexOutOfBoundsException (数组越界异常),当访问数组元素时索引超出数组范围就会抛出。虽然非受检异常不需要强制捕获,但良好的编程习惯要求对可能出现的这类异常进行适当处理,以增强程序的健壮性。
总体而言,Error 代表 JVM 难以处理的严重系统问题,而 Exception 则是程序运行中可预期并能够处理的异常情况,通过合理的异常处理机制,能够使程序在遇到异常时仍保持一定的稳定性和可靠性。
说明多态、重载和重写的概念
- 多态:多态是面向对象编程的重要特性之一,它允许不同类的对象对同一消息作出不同的响应。简单来说,就是用父类的引用指向子类的对象,在运行时根据实际对象的类型来决定调用哪个子类的方法。多态主要通过继承和方法重写来实现。例如,有一个动物类
Animal,它有一个方法makeSound,子类Dog和Cat继承自Animal,并各自重写了makeSound方法。当创建Dog和Cat的对象,并使用Animal类型的引用指向它们时,调用makeSound方法就会执行各自子类重写后的方法,实现不同的声音效果。多态提高了代码的灵活性和可扩展性,使得程序可以更方便地处理不同类型的对象。 - 重载:重载是指在同一个类中,多个方法可以有相同的名称,但参数列表必须不同(参数个数、类型或顺序不同)。例如,在一个数学计算类中,可能有一个
add方法用于两个整数相加,还可以有另一个add方法用于两个浮点数相加,或者用于一个整数和一个浮点数相加。编译器会根据调用方法时传递的参数类型和数量来决定调用哪个重载方法。重载使得代码更加简洁,方便用户通过相同的方法名完成不同类型数据的类似操作,提高了代码的可读性和复用性。 - 重写:重写发生在子类与父类之间,当子类需要对从父类继承的方法进行不同的实现时,就可以重写该方法。重写的方法必须与父类中被重写的方法具有相同的方法名、返回类型(或是其子类型)和参数列表。例如,上述
Animal类中的makeSound方法在Dog子类中重写,Dog类的makeSound方法实现狗叫的逻辑,而不是沿用Animal类中通用的(可能比较简单或抽象的)声音实现逻辑。重写体现了继承关系中,子类对父类方法的定制化,符合面向对象编程中对特殊情况特殊处理的思想,同时也遵循了里氏替换原则,保证了继承体系的一致性和扩展性。
介绍 Java 里面的异常
Java 中的异常是指程序在运行过程中出现的错误或异常情况,这些情况会打断程序的正常执行流程。异常机制为 Java 程序提供了一种有效的错误处理方式,使得程序更加健壮和可靠。
Java 异常体系结构的根类是 Throwable ,它有两个直接子类: Error 和 Exception 。
Error 表示系统级别的错误,如前面提到的 OutOfMemoryError 和 StackOverflowError ,这类错误通常是由于 JVM 内部问题或系统资源耗尽导致,应用程序通常无法恢复或处理,一旦发生往往会导致 JVM 崩溃。
Exception 则表示程序运行时发生的、可以被捕获和处理的异常情况。它又进一步分为受检异常(Checked Exception)和非受检异常(Unchecked Exception)。
受检异常要求在编译时进行处理,编译器会强制检查此类异常是否被捕获或声明抛出。例如, IOException ,当进行文件操作或网络操作时,如果出现文件不存在、网络连接中断等问题,就可能抛出 IOException 。程序必须使用 try - catch 块捕获该异常,或者在方法声明中使用 throws 关键字声明抛出该异常,否则无法通过编译。这种机制使得开发者在编写代码时必须考虑到可能出现的异常情况,并进行相应的处理,从而提高了程序的稳定性和可靠性。
非受检异常不需要在编译时显式处理,它们通常是由于编程错误导致,如 NullPointerException ,当试图访问一个空对象的方法或属性时会抛出;ArrayIndexOutOfBoundsException ,当访问数组元素时索引超出数组范围就会抛出。虽然非受检异常不需要强制捕获,但在开发过程中应该尽量避免这类异常的发生,通过合理的代码逻辑和检查来预防。例如,在访问对象之前先检查对象是否为 null ,在访问数组元素之前先检查索引是否在有效范围内。
通过合理使用 Java 的异常机制,在程序出现异常时,可以捕获异常并进行相应的处理,如记录错误日志、给用户友好的提示等,避免程序突然崩溃,提高用户体验。同时,良好的异常处理也有助于调试和维护代码,使得开发人员能够快速定位和解决问题。
手写单例模式,说明 volatile 的作用,为什么要双重判空,其作用是什么?
单例模式实现: 单例模式是一种设计模式,确保一个类只有一个实例,并提供一个全局访问点。下面是一个经典的双重检查锁定实现的单例模式代码:
public class Singleton {
// 使用volatile关键字修饰单例实例
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;
}
}
volatile 的作用: volatile 关键字的主要作用是保证变量的可见性和禁止指令重排序。在上述单例模式中,当 instance 被 volatile 修饰时,任何线程对 instance 的修改都会立即被其他线程看到。这是因为 volatile 变量在修改时会直接刷新到主内存,读取时也会从主内存读取,而不是从线程的本地缓存中读取。如果不使用 volatile ,在多线程环境下,可能会出现一个线程已经创建了 instance ,但另一个线程由于本地缓存的原因,仍然认为 instance 为 null ,从而再次创建实例,导致违反单例模式的原则。
双重判空的作用: 第一次判空 if (instance == null) 主要是为了提高效率。如果每次调用 getInstance() 方法都进行同步操作,会带来较大的性能开销。在大多数情况下, instance 已经被创建,通过第一次判空可以直接返回已有的实例,避免进入同步块。
第二次判空 if (instance == null) 则是在同步块内部,用于确保在多线程环境下只有一个实例被创建。当多个线程同时通过第一次判空并进入同步块时,如果没有第二次判空,可能会有多个线程创建实例。通过第二次判空,只有第一个进入同步块的线程会创建实例,后续线程进入同步块时会发现 instance 已经不为 null ,从而直接返回已创建的实例。
综上所述, volatile 关键字和双重判空机制在单例模式的实现中,共同保证了在多线程环境下,单例模式的正确性和高效性。
解释为什么要泛型擦除,写一个泛型方法和一个泛型类
泛型擦除的原因: Java 的泛型是在编译期实现的,泛型擦除是 Java 为了兼容旧版本代码而采用的一种机制。在 Java 5.0 之前并没有泛型,为了使引入泛型后的代码能够与旧版本代码兼容,同时保证运行时的性能不受太大影响,Java 采用了泛型擦除。
具体来说,在编译过程中,所有的泛型类型信息都会被擦除,只保留原始类型。例如, List<String> 在编译后会变成 List ,编译器会在适当的地方插入类型转换代码。这样做使得使用泛型的代码在运行时与没有泛型的代码具有相同的字节码结构,从而可以在不改变 JVM 的情况下实现泛型功能。
另外,泛型擦除也有助于减少代码膨胀。如果不进行擦除,对于每一种泛型类型参数,都需要生成一套独立的字节码,这会导致类文件数量大幅增加,占用更多的存储空间和内存。通过泛型擦除,只需要一份字节码就可以处理不同类型参数的情况,提高了代码的复用性和运行效率。
泛型方法示例:
public class GenericMethods {
// 泛型方法,用于交换数组中两个元素的位置
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
在上述代码中, <T> 表示这是一个泛型方法, T 是类型参数,可以代表任何类型。该方法可以用于交换任何类型数组中指定位置的两个元素。
泛型类示例:
public class GenericBox<T> {
private T value;
public GenericBox(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
在这个 GenericBox 类中, <T> 是类型参数, T 可以代表任何类型。该类可以存储一个指定类型的值,并提供获取和设置该值的方法。通过使用泛型类,可以使代码更加通用,适用于不同的数据类型,而不需要为每种类型都创建一个单独的类。
分析 sleep () 和 wait () 的区别
- 所属类不同:
sleep()方法是Thread类的静态方法,而wait()方法是Object类的实例方法。这意味着sleep()可以直接通过Thread类调用,用来让当前线程暂停执行一段时间;而wait()需要通过对象来调用,用于实现线程之间的通信和协作。 - 释放锁的情况不同:当线程执行
sleep()方法时,它不会释放当前持有的锁,其他线程仍然无法访问被该线程锁定的资源。而线程执行wait()方法时,会释放当前持有的对象锁,使得其他线程有机会获取该锁并访问相应的资源。 - 唤醒方式不同:
sleep()方法在指定的睡眠时间到达后,线程会自动唤醒并继续执行。而wait()方法需要其他线程调用同一个对象的notify()或notifyAll()方法来唤醒等待的线程。如果没有其他线程进行唤醒操作,那么调用wait()的线程将一直处于等待状态。 - 使用场景不同:
sleep()通常用于让线程暂停一段时间,比如在定时任务中,每隔一段时间执行一次某个操作。而wait()常用于多线程之间的协作,比如生产者 - 消费者模型中,当缓冲区满时,生产者线程调用wait()等待,当缓冲区有空间时,消费者线程消费后调用notify()通知生产者线程继续生产。
有 5 个线程,要等它们执行完再去执行另外的线程,怎么实现
- 使用 CountDownLatch:
CountDownLatch是 Java 并发包中的一个工具类,它可以实现让一个或多个线程等待其他线程完成操作后再继续执行。首先创建一个CountDownLatch对象,初始值为 5,代表有 5 个线程需要完成任务。每个线程在执行完毕后调用countDown()方法,将计数器减 1。主线程或其他需要等待的线程调用await()方法,会阻塞直到计数器的值为 0,即 5 个线程都执行完。示例代码如下:
import java.util.concurrent.CountDownLatch;
public class ThreadWaitExample {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
final int index = i;
new Thread(() -> {
System.out.println("线程 " + index + " 开始执行");
// 模拟线程执行任务
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程 " + index + " 执行完毕");
latch.countDown();
}).start();
}
latch.await();
System.out.println("所有线程执行完毕,开始执行后续操作");
}
}
- 使用 Thread.join ():
join()方法可以让当前线程等待调用该方法的线程执行完毕后再继续执行。创建 5 个线程并启动,然后在主线程中依次调用每个线程的join()方法,这样主线程就会等待这 5 个线程都执行完后再继续执行后续代码。示例代码如下:
public class ThreadJoinExample {
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[5];
for (int i = 0; i < 5; i++) {
final int index = i;
threads[i] = new Thread(() -> {
System.out.println("线程 " + index + " 开始执行");
// 模拟线程执行任务
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程 " + index + " 执行完毕");
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println("所有线程执行完毕,开始执行后续操作");
}
}
说明软引用和弱引用的区别
- 定义及回收时机:软引用是一种相对较强的引用关系。当系统内存不足时,垃圾回收器才会回收被软引用指向的对象。如果内存足够,软引用对象会一直存在。而弱引用是一种更弱的引用关系,只要垃圾回收器扫描到了弱引用对象,不管当前内存是否充足,都会立即回收该对象。
- 使用场景:软引用常用于缓存场景,比如内存中的图片缓存。当内存充足时,图片可以一直保存在缓存中,方便下次使用;当内存紧张时,系统会自动回收这些软引用指向的图片,以释放内存。弱引用则常用于解决内存泄漏问题,比如在观察者模式中,如果观察者持有被观察对象的强引用,可能会导致被观察对象无法被释放。使用弱引用可以让被观察对象在没有其他强引用时,能够被及时回收。
- 在 Java 中的实现:在 Java 中,软引用通过
SoftReference类来实现,弱引用通过WeakReference类来实现。例如:
// 软引用示例
Object object = new Object();
SoftReference<Object> softReference = new SoftReference<>(object);
// 当内存不足时,softReference.get()可能会返回null
// 弱引用示例
Object object2 = new Object();
WeakReference<Object> weakReference = new WeakReference<>(object2);
// 当垃圾回收器运行后,weakReference.get()很可能会返回null
说明 volatile 的作用,synchronized 的作用范围
- volatile 的作用:
volatile关键字主要有两个作用。一是保证变量的可见性,当一个线程修改了volatile修饰的变量的值,其他线程能够立即看到这个修改。这是因为volatile会强制线程从主内存中读取变量的值,而不是从线程的工作内存中读取,从而避免了数据不一致的问题。二是禁止指令重排序,在多线程环境下,编译器和处理器可能会对指令进行重排序以提高性能,但这可能会导致一些逻辑错误。volatile修饰的变量会禁止这种重排序,保证代码按照编写的顺序执行。 - synchronized 的作用范围:
synchronized关键字可以用于修饰方法和代码块。当修饰实例方法时,它会对当前对象进行加锁,确保在同一时刻只有一个线程能够访问该方法。当修饰静态方法时,它会对当前类的Class对象进行加锁,因为静态方法属于类而不是实例,所以所有实例调用该静态方法时都会受到锁的限制。当修饰代码块时,可以指定一个对象作为锁对象,只有获取到该锁对象的线程才能执行代码块中的内容,这样可以更精细地控制锁的范围,提高并发性能。例如:
public class SynchronizedExample {
// 实例方法加锁
public synchronized void instanceMethod() {
// 方法体
}
// 静态方法加锁
public static synchronized void staticMethod() {
// 方法体
}
// 代码块加锁
public void blockMethod() {
Object lock = new Object();
synchronized (lock) {
// 代码块内容
}
}
}
如何将异步结果同步返回
- 使用 Future 和 Callable:
Callable是一个泛型接口,它的call()方法可以有返回值,用于执行异步任务并返回结果。Future表示异步计算的结果,可以通过Future的get()方法获取异步任务的返回值,在调用get()方法时,如果异步任务还没有完成,当前线程会阻塞,直到异步任务完成并返回结果。示例代码如下:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class AsyncToSyncExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建Callable任务
Callable<Integer> callable = () -> {
// 模拟异步任务执行
Thread.sleep(2000);
return 42;
};
// 创建FutureTask并传入Callable
FutureTask<Integer> futureTask = new FutureTask<>(callable);
// 启动线程执行异步任务
new Thread(futureTask).start();
// 获取异步任务的结果,如果任务未完成,会阻塞当前线程
Integer result = futureTask.get();
System.out.println("异步任务结果:" + result);
}
}
- 使用 CompletableFuture:
CompletableFuture是 Java 8 引入的一个类,它提供了更强大的异步编程支持。可以通过supplyAsync()方法创建一个异步任务,然后通过join()或get()方法获取异步任务的结果。CompletableFuture还支持链式调用和多种异步操作组合。示例代码如下:
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExample {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 创建异步任务
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
// 模拟异步任务执行
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 42;
});
}
分析 java 权限符(public 等)及其访问范围
Java 中有四种权限修饰符,分别是public、protected、default(默认,即不写任何修饰符)和private,它们的访问范围各有不同。
public:具有最大的访问权限,被public修饰的类、方法、变量等可以在任何地方被访问,无论是在同一个类中、同一个包中还是不同的包中,只要能获取到相应的引用,都可以进行访问。例如一个public的工具类中的public方法,在整个项目的任何角落都可以被调用。protected:如果成员被protected修饰,那么在同一个类中、同一个包中的其他类以及不同包中的子类都可以访问它。比如在一个基础类中有一个protected方法,它的子类即使在不同包中也可以重写或调用这个方法。default:即默认权限,当没有任何修饰符时就是默认权限。具有默认权限的成员只能在同一个包内被访问。不同包中的类,即使是子类也无法访问默认权限的成员。private:private修饰的成员具有最小的访问权限,只能在当前类内部被访问和使用,其他任何类都无法直接访问private成员。比如一个类中的private变量,只能在该类的方法中进行操作,外部类无法直接获取或修改它。
说明双亲委派机制
双亲委派机制是 Java 类加载器的一种工作模式。简单来说,当一个类加载器收到类加载请求时,它不会立即去加载这个类,而是先将请求委托给它的父类加载器去加载,父类加载器又会将请求继续向上委托,直到到达顶层的启动类加载器。 如果父类加载器能够加载这个类,就由父类加载器完成加载;只有当父类加载器无法加载时,子类加载器才会尝试自己去加载。例如,对于 Java 核心类库中的类,如java.lang.String,当应用程序中的类加载器需要加载它时,会一直向上委托到启动类加载器,因为启动类加载器负责加载 Java 核心类库,它能够找到并加载String类,所以就由启动类加载器完成加载,而不会由应用程序类加载器去加载。 这种机制的好处在于保证了 Java 核心类库的安全性和稳定性,避免了用户自定义的类与核心类库中的类产生冲突,也保证了类加载的层次关系和一致性。
分析 Synchronized 和 lock 的区别
Synchronized和Lock都用于实现多线程中的同步和互斥,但它们有一些区别。
- 实现方式:
Synchronized是 Java 语言的关键字,是基于 JVM 层面实现的,它的加锁和释放锁是自动的。而Lock是一个接口,是基于 Java 代码层面实现的,需要手动调用lock()方法加锁和unlock()方法释放锁,通常配合try-finally语句块来确保锁能正确释放。 - 锁的获取方式:
Synchronized在获取锁时,如果锁被占用,线程会一直阻塞等待。Lock可以通过tryLock()方法尝试获取锁,如果获取不到可以立即返回或者等待一段时间后返回,不会一直阻塞。 - 锁的类型:
Synchronized是可重入锁,即同一个线程可以多次获取同一个锁。Lock不仅可以实现可重入锁,还可以实现公平锁和非公平锁等更灵活的锁类型,通过构造函数传入不同的参数来指定。 - 锁的粒度:
Synchronized可以作用于方法和代码块,粒度相对较粗。Lock可以更灵活地控制锁的粒度,可以在代码中的任何位置进行加锁和解锁操作,能实现更细粒度的锁控制。
介绍 Java 的四种引用类型,什么时候会被回收,项目里怎么用的
Java 中有四种引用类型,分别是强引用、软引用、弱引用和虚引用。
- 强引用:是最常见的引用类型,如
Object obj = new Object();中obj就是强引用。只要强引用存在,垃圾回收器就不会回收被引用的对象,即使内存不足也不会回收,可能会导致内存溢出。在项目中,大多数普通对象都是通过强引用来使用的,用于保证对象在需要的时候一直存在。 - 软引用:通过
SoftReference类来实现,当内存空间足够时,软引用对象不会被回收;当内存不足时,垃圾回收器会回收软引用指向的对象。常用于实现缓存机制,比如图片缓存,当内存充足时可以保留图片数据,内存紧张时可以释放这些缓存以避免内存溢出。 - 弱引用:由
WeakReference类表示,无论内存是否充足,只要垃圾回收器扫描到弱引用对象,就会回收它。可以用于解决内存泄漏问题,例如在HashMap中使用弱引用作为键,当键对象不再被其他地方强引用时,就可以被垃圾回收器回收,防止内存泄漏。 - 虚引用:通过
PhantomReference类实现,虚引用不会影响对象的生命周期,对对象本身没有实际的引用作用,主要用于跟踪对象被垃圾回收的状态,通常与ReferenceQueue配合使用,在对象被回收时收到通知,进行一些资源释放等操作。
泛型中 extend 和 super 的区别?泛型擦除是什么?
在 Java 泛型中,extends和super是用于限定泛型类型边界的关键字,它们的区别如下:
extends:用于上界限定,表示泛型类型必须是指定类型的子类或本身。例如<? extends T>,这里的T是一个上限,实际传入的类型只能是T或者T的子类。在方法中,如果参数使用了extends限定,那么在方法内部可以调用T中定义的方法,但不能向这个参数中添加除null以外的任何元素,因为无法确定具体的类型,可能会导致类型不匹配。super:用于下界限定,<? super T>表示泛型类型必须是指定类型T的父类或本身。在方法中,如果参数使用了super限定,那么可以向这个参数中添加T类型或T的子类类型的元素,因为父类肯定能容纳子类对象,但在读取时只能得到Object类型,因为无法确定具体的父类类型。
泛型擦除是 Java 泛型实现中的一个概念。Java 的泛型是在编译时实现的,在编译过程中,会将泛型类型信息擦除,替换为原始类型。例如,List<String>在编译后实际上是List,编译器会在编译时插入必要的类型检查和类型转换代码来保证类型安全。这是为了保持 Java 的兼容性,因为在 Java 5 之前没有泛型,为了让使用泛型的代码能够在老版本的 JVM 上运行,就采用了泛型擦除机制。但这种机制可能会导致一些问题,比如在运行时无法获取到泛型的具体类型信息等。
说明 final 关键字的作用
- 修饰类:当
final修饰一个类时,表示这个类不能被继承。例如,Java 中的String类就是被final修饰的,这保证了String类的不可变性和安全性,防止其他类通过继承来修改String类的行为和特性。 - 修饰方法:用
final修饰的方法不能在子类中被重写。这有助于确保方法的行为在继承体系中保持一致,防止子类意外地改变父类中关键方法的实现。比如,Object类中的getClass方法就是final的,因为它的行为是基于对象的运行时类型,不应该被重写。 - 修饰变量:如果
final修饰的是基本数据类型的变量,那么该变量的值一旦被初始化就不能再改变;如果修饰的是引用类型的变量,那么该变量所指向的对象地址不能被改变,但对象内部的属性是可以改变的。例如:final int num = 10;,num的值就不能再被修改。
接口和抽象类的区别是什么
- 定义:接口是一种完全抽象的类型,它只包含方法签名和常量,方法默认是
public和abstract的,常量默认是public static final的。抽象类是一种既有抽象方法又可能有具体方法和成员变量的类。 - 实现:一个类可以实现多个接口,而一个类只能继承一个抽象类。
- 方法:接口中的方法不能有方法体,而抽象类中的方法可以有方法体,也可以没有。
- 变量:接口中只能有常量,不能有普通变量,而抽象类中可以有各种类型的成员变量。
- 作用:接口常用于定义一组相关的行为规范,强调的是行为的一致性和可替换性。抽象类更多地用于提取公共的代码和逻辑,作为继承体系中的公共父类,为子类提供通用的实现框架。
介绍 Java 多线程相关知识
- 线程的创建:可以通过继承
Thread类或实现Runnable接口来创建线程。继承Thread类时,需要重写run方法;实现Runnable接口时,也需要实现run方法,然后将实现了Runnable接口的对象作为参数传递给Thread类的构造函数来创建线程。 - 线程的状态:Java 线程有新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)五种状态。新建状态是线程被创建但还未启动;就绪状态是线程已经获得了除 CPU 之外的所有资源,等待 CPU 调度;运行状态是线程正在执行
run方法中的代码;阻塞状态是线程因为等待某个条件或资源而暂停执行;死亡状态是线程执行完毕或因异常退出。 - 线程的同步:当多个线程同时访问共享资源时,可能会导致数据不一致等问题。可以使用
synchronized关键字或Lock接口来实现线程同步,确保在同一时刻只有一个线程能够访问共享资源。 - 线程间通信:可以使用
wait、notify和notifyAll方法来实现线程间的通信,使线程之间能够协调工作。例如,生产者 - 消费者模型中,生产者线程生产数据后通过notify通知消费者线程消费数据,消费者线程在没有数据时通过wait等待生产者线程生产数据。
线程池有哪些种类
- FixedThreadPool:固定线程数量的线程池,线程池中的线程数量是固定的,不会随着任务的提交而增加或减少。适用于处理大量的、相对稳定的任务,例如服务器中的请求处理线程池。
- CachedThreadPool:可缓存的线程池,线程池中的线程数量会根据任务的提交动态调整。如果线程池中有空闲线程,则复用空闲线程;如果没有空闲线程,则创建新的线程。适用于处理短期的、突发性的大量任务。
- SingleThreadExecutor:单线程的线程池,线程池中只有一个线程,所有任务按照提交的顺序依次执行。适用于需要保证任务顺序执行,且不需要并发执行的场景。
- ScheduledThreadPool:定时任务线程池,用于执行定时任务和周期性任务。可以指定任务在特定的时间延迟后执行,或者按照一定的时间间隔周期性地执行。适用于定时备份数据、定时发送邮件等任务。
线程池的 handler 拒绝策略有哪些
- AbortPolicy:这是默认的拒绝策略。当线程池无法处理新的任务时,会抛出
RejectedExecutionException异常,拒绝接收新任务。 - CallerRunsPolicy:当线程池拒绝任务时,会将任务回退到调用者线程中执行,由调用者线程来处理任务。这样可以保证任务不会丢失,但可能会影响调用者线程的正常执行。
- DiscardPolicy:直接丢弃无法处理的任务,不做任何处理。这种策略可能会导致任务丢失,一般在对任务可靠性要求不高的场景下使用。
- DiscardOldestPolicy:丢弃线程池中等待时间最长的任务,然后尝试将新任务加入线程池。如果线程池仍然无法处理新任务,则继续丢弃等待时间最长的任务,直到能够处理新任务或者线程池关闭。
分析 Threads 和 Runnable 的区别
在 Java 中,Thread和Runnable是实现多线程的两种重要方式,它们存在诸多区别。
从定义和本质来看,Thread类是 Java 中用于创建和操作线程的类,它本身就代表一个线程;而Runnable是一个接口,它只是定义了一个可以被线程执行的任务,更侧重于描述线程要执行的内容。
在使用方式上,若继承Thread类,就需要创建一个Thread类的子类,并重写run方法来定义线程的执行逻辑;若实现Runnable接口,则需要在类中实现run方法,然后将这个实现类的实例作为参数传递给Thread类的构造函数来创建线程。相比之下,实现Runnable接口更灵活,因为 Java 不支持多继承,若一个类已经继承了其他类,就无法再继承Thread类,但可以实现Runnable接口来实现多线程功能。
从资源共享角度来说,使用Thread类时,每个线程都是独立的对象,如果要实现多个线程之间的资源共享,需要额外的机制来处理;而使用Runnable接口时,可以很方便地实现多个线程对同一资源的共享,因为多个线程可以共享同一个实现了Runnable接口的实例。
改写 equals () 为什么要改写 hashcode ()
在 Java 中,equals()方法和hashCode()方法是紧密相关的,重写equals()方法时通常也需要重写hashCode()方法。
这是因为 Java 中的集合类,如HashMap、HashSet等,是根据对象的哈希码来存储和查找对象的。hashCode()方法用于返回对象的哈希码值,当向哈希表中存储对象时,会根据对象的哈希码值来确定对象在哈希表中的存储位置。equals()方法用于判断两个对象是否相等。
如果只重写equals()方法而不重写hashCode()方法,可能会导致两个在逻辑上相等的对象具有不同的哈希码。比如,自定义了一个类Person,重写了equals()方法根据姓名和年龄来判断两个Person对象是否相等,但没有重写hashCode()方法。那么当把两个姓名和年龄都相同的Person对象放入HashSet中时,HashSet可能会认为这是两个不同的对象,因为它们的哈希码不同,从而导致集合中出现了逻辑上重复的元素,这与集合的不重复特性相违背。
按照 Java 的规范,如果两个对象通过equals()方法比较返回true,那么它们的hashCode()方法返回的值必须相等,这样才能保证在使用哈希表相关的集合类时,对象的存储和查找等操作能够正确进行。
解释 synchronized 不可重入锁,怎么理解?
在 Java 中,synchronized关键字实现的锁是可重入锁,而不是不可重入锁,可从以下几个方面理解。
可重入锁意味着同一个线程在已经获取了锁的情况下,可以再次获取该锁而不会被阻塞。比如,一个线程调用了一个 synchronized修饰的方法method1,在method1方法内部又调用了另一个synchronized修饰的方法method2,如果是不可重入锁,那么线程在调用method2时会被阻塞,因为它已经持有了method1方法的锁,无法再次获取method2方法的锁,但synchronized修饰的锁不会出现这种情况。
从实现原理上来说,Java 的对象头中会有一些标记位来记录锁的状态等信息,对于可重入锁,当线程获取锁时,会在锁的记录中增加一个计数器,每重入一次,计数器就加 1,当线程释放锁时,计数器减 1,只有当计数器为 0 时,才真正释放锁。
这种可重入性的设计有很多好处,它避免了死锁的发生,使得代码在进行方法调用等操作时更加灵活和安全。比如在继承关系中,子类的 synchronized方法可能会调用父类的synchronized方法,如果不是可重入锁,就很容易导致死锁。
说明 synchronized 修饰在普通方法和类方法上的区别
synchronized关键字修饰在普通方法和类方法上有明显区别。
当synchronized修饰普通方法时,它作用于当前对象实例。也就是说,只有当前对象的锁被获取时,才能执行被synchronized修饰的普通方法。不同的对象实例之间的synchronized普通方法是相互独立的,它们各自持有自己的对象锁。例如,有两个MyClass类的实例obj1和obj2,obj1调用synchronized修饰的普通方法时,并不会影响obj2调用同一个synchronized普通方法,因为它们获取的是各自对象的锁。
而当synchronized修饰类方法(即static方法)时,它作用于类的Class对象。因为类方法是属于类的,而不是属于某个具体的对象实例,所以类方法的锁是针对整个类的。在这种情况下,无论有多少个类的实例,只要有一个线程获取了类方法的锁,其他线程在该锁被释放之前,都无法访问这个类的任何synchronized类方法。比如,对于MyClass类的synchronized类方法,不管是通过obj1还是obj2或者其他任何方式调用,只要有一个线程正在执行这个类方法,其他线程就必须等待。
Java 中哪些结构中存储数据不能重复?
在 Java 中,有不少数据结构存储数据时具有不重复的特性。
首先是Set接口及其实现类,如HashSet、TreeSet等。Set集合的主要特点就是不允许存储重复元素。以HashSet为例,它是基于哈希表实现的,当向HashSet中添加元素时,会根据元素的哈希码值来确定元素在哈希表中的存储位置,如果要添加的元素与已存在的元素哈希码相同且通过equals()方法比较相等,那么新元素就不会被添加进去。TreeSet则是基于红黑树实现的,它会按照元素的自然顺序或自定义的比较器顺序来存储元素,同样不允许有重复元素。
其次是Map接口的一些实现类中的键(Key),比如HashMap、TreeMap等。在Map中,键是唯一的,不能重复,而值(Value)可以重复。这是因为Map是通过键来查找和访问值的,如果键可以重复,就无法准确地获取对应的 value 值。例如在HashMap中,键的唯一性也是通过哈希码和equals()方法来保证的,与HashSet的原理类似。
还有EnumSet,它是专门用于存储枚举类型元素的集合,同样不允许元素重复,并且它针对枚举类型做了优化,具有高效的存储和访问性能。
说明 Overload 和 Override 的区别
- 定义
- Overload(重载):指在同一个类中,允许存在多个同名方法,但这些方法的参数列表不同(参数个数、参数类型或参数顺序不同)。编译器会根据调用方法时传入的参数来决定调用哪个重载方法。
- Override(重写):是指子类中重新定义了父类中已存在的方法,方法名、参数列表、返回值类型必须与父类中的方法完全相同(在 Java 中,返回值类型可以是父类方法返回值类型的子类,这是一种协变返回类型的情况)。
- 目的与作用
- Overload:主要用于提供功能相似但参数不同的方法,方便用户根据不同的输入进行不同的操作,增强了方法的灵活性和可扩展性。
- Override:用于子类根据自身的需求对父类的方法进行重新实现,体现了多态性,使得子类可以在继承父类的基础上,对父类的行为进行修改或扩展。
- 其他特性
- Overload:与方法的访问修饰符、返回值类型无关,只看参数列表。
- Override:子类重写父类方法时,不能比父类中被重写的方法更严格地限制访问级别,例如父类方法是
public,子类重写时不能改为private或protected。
阐述 View 的事件分发机制
- View 的事件分发主要涉及三个方法:
dispatchTouchEvent()、onInterceptTouchEvent()和onTouchEvent()。 - dispatchTouchEvent():用于分发触摸事件。如果返回
true,表示该 View 处理了事件,不再继续分发;如果返回false,则表示不处理,将事件传递给父容器的onTouchEvent()方法;如果返回super.dispatchTouchEvent(),则按照默认规则继续分发事件。 - onInterceptTouchEvent():用于拦截触摸事件,只有 ViewGroup 才有这个方法。如果返回
true,表示拦截事件,接下来事件会交给该 ViewGroup 的onTouchEvent()方法处理;如果返回false,则表示不拦截,事件会继续传递给子 View 的dispatchTouchEvent()方法。 - onTouchEvent():用于处理触摸事件。返回
true表示消费了事件,不再向上传递;返回false表示不处理,事件会向上传递给父容器的onTouchEvent()方法。 - 事件分发的顺序是从 Activity 的
dispatchTouchEvent()开始,到 ViewGroup 的dispatchTouchEvent(),再到 ViewGroup 的onInterceptTouchEvent(),然后可能到子 View 的dispatchTouchEvent()和onTouchEvent(),最后如果子 View 不处理,再回到父 ViewGroup 的onTouchEvent(),如果父 ViewGroup 也不处理,最终会回到 Activity 的onTouchEvent()。
介绍 Activity 的启动模式
- standard:标准模式,每次启动一个 Activity 都会创建一个新的实例,无论这个 Activity 是否已经存在于栈中。这种模式下,多个相同的 Activity 可以同时存在于任务栈中。
- singleTop:栈顶复用模式,如果要启动的 Activity 已经位于任务栈的栈顶,那么不会创建新的实例,而是复用栈顶的 Activity,并调用其
onNewIntent()方法。如果不在栈顶,则会创建新的实例。 - singleTask:栈内复用模式,系统会检查要启动的 Activity 在任务栈中是否已经存在,如果存在,则将该 Activity 上面的所有 Activity 出栈,使该 Activity 成为栈顶元素,并调用其
onNewIntent()方法;如果不存在,则创建新的实例放入栈中。 - singleInstance:单实例模式,这种模式下,会为该 Activity 创建一个单独的任务栈,并且这个任务栈中只有这一个 Activity 实例。其他应用组件启动该 Activity 时,都会复用这个单独任务栈中的实例。
自定义 View 的步骤是什么?需要重写哪些方法?
- 自定义 View 的步骤
- 继承 View 或其子类:如果是简单的自定义 View,通常继承 View 类;如果要实现类似于 TextView、Button 等具有特定功能的 View,可以继承相应的子类。
- 在构造方法中进行初始化:例如初始化一些属性、加载自定义的样式等。
- 重写测量方法:根据 View 的内容和布局需求,重写
onMeasure()方法来确定 View 的测量宽高。 - 重写布局方法:如果是 ViewGroup,需要重写
onLayout()方法来确定子 View 的位置和大小;如果是 View,一般不需要重写此方法。 - 重写绘制方法:通过重写
onDraw()方法来绘制 View 的内容,使用Canvas和Paint等类进行图形绘制。 - 处理触摸事件等交互:根据需要,重写
onTouchEvent()等方法来处理用户的触摸等交互操作。
- 需要重写的方法
- onMeasure(int widthMeasureSpec, int heightMeasureSpec):用于测量 View 的宽高。
- onDraw(Canvas canvas):用于在 View 上进行绘制操作。
- onLayout(boolean changed, int left, int top, int right, int bottom):ViewGroup 需要重写此方法来确定子 View 的布局。
- onTouchEvent(MotionEvent event):用于处理触摸事件。
如何自定义属性?
- 在 res/values 目录下创建 attrs.xml 文件:在该文件中定义自定义属性,例如:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CustomView">
<attr name="customText" format="string" />
<attr name="customColor" format="color" />
</declare-styleable>
</resources>
这里定义了一个名为CustomView的样式组,其中包含了customText和customColor两个自定义属性,分别为字符串类型和颜色类型。
- 在布局文件中使用自定义属性:在布局文件中使用自定义 View 时,可以设置这些自定义属性,例如:
<com.example.CustomView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:customText="Hello World"
app:customColor="@color/colorPrimary" />
这里假设CustomView是自定义的 View 类,通过app:命名空间来使用自定义属性。
- 在自定义 View 的构造方法中获取属性值:在自定义 View 的构造方法中,可以通过
TypedArray来获取自定义属性的值,例如:
public class CustomView extends View {
private String customText;
private int customColor;
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CustomView);
customText = ta.getString(R.styleable.CustomView_customText);
customColor = ta.getColor(R.styleable.CustomView_customColor, Color.BLACK);
ta.recycle();
}
// 其他方法...
}
这样就可以在自定义 View 中使用自定义属性的值了。
阐述线性布局和相对布局的特点
- 线性布局:线性布局是一种按照水平或垂直方向排列子视图的布局方式。如果是水平方向,子视图会从左到右依次排列;垂直方向则从上到下排列。它的一大特点是简单直观,开发者可以很容易地控制子视图的排列顺序。通过设置
android:orientation属性来指定排列方向。而且,线性布局支持权重(weight)属性,允许开发者根据需求分配子视图在布局中的空间占比。比如在一个水平线性布局中,有两个按钮,可通过设置权重让一个按钮占屏幕宽度的三分之一,另一个占三分之二。不过,线性布局在复杂界面布局时可能会有局限性,若有多个子视图需要复杂的排列和对齐方式,可能需要嵌套多个线性布局,这会增加布局的复杂度和层级。 - 相对布局:相对布局允许子视图通过相对位置来进行排列,可以相对于父布局或者其他子视图进行定位。例如,一个按钮可以设置为相对于父布局的左上角、右上角等位置,也可以设置为在另一个按钮的上方、下方、左侧、右侧等。这种布局方式非常灵活,能够实现各种复杂的布局效果,减少布局的嵌套层级,提高布局的性能。但相对布局的缺点是,当界面较为复杂时,设置子视图的相对位置可能会比较繁琐,需要仔细计算和调整各个视图之间的关系,对开发者的布局能力要求较高。
解释 dpi、px 和 sp 的含义及区别
- dpi(Dots Per Inch):即每英寸点数,它是描述单位长度内像素点数量的物理量,主要用于衡量屏幕的像素密度。例如,常见的手机屏幕可能有 300dpi、400dpi 等不同的像素密度。dpi 的值越高,屏幕显示的图像就越清晰细腻。它与设备的硬件特性相关,不同设备的 dpi 可能不同,是一个与物理尺寸和像素数量相关的概念。
- px(Pixel):即像素,是图像显示的基本单位,一个像素对应屏幕上的一个点。在进行 Android 开发时,以 px 为单位设置视图的尺寸和位置等,在不同 dpi 的设备上可能会显示出不同的大小和效果。比如在低 dpi 的设备上,同样是 100px 宽的按钮,会比在高 dpi 设备上显得更大。
- sp(Scale-independent Pixel):即独立缩放像素,主要用于字体大小的设置。它会根据用户设置的字体大小偏好以及设备的屏幕密度等因素进行自适应缩放。使用 sp 作为字体单位,可以确保在不同设备和不同字体大小设置下,字体都能保持合适的显示效果,不会出现字体过大或过小的情况。
三者的区别在于,dpi 是描述屏幕像素密度的物理量,px 是图像的基本显示单位,在不同设备上显示效果可能不同,而 sp 主要用于字体,能根据设备和用户设置进行自适应缩放,以保证字体显示的合理性和舒适性。
介绍 Android 的几种动画器及使用方法
- 补间动画(Tween Animation):补间动画是通过对 View 的平移、旋转、缩放、透明度等属性进行动画操作,实现从一个状态到另一个状态的过渡效果。可以在 XML 文件中定义动画,如定义一个平移动画:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromXDelta="0"
android:toXDelta="100"
android:fromYDelta="0"
android:toYDelta="100"
android:duration="1000" />
</set>
然后在 Java 代码中通过AnimationUtils.loadAnimation()方法加载动画并应用到 View 上。
- 属性动画(Property Animation):属性动画可以对任何对象的属性进行动画操作,不仅仅局限于 View。它可以实现更复杂和灵活的动画效果,比如可以自定义属性的动画过程。使用属性动画可以直接在 Java 代码中创建,例如:
ObjectAnimator animator = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f, 1f);
animator.setDuration(2000);
animator.start();
这段代码实现了一个 View 的透明度从 1 到 0 再到 1 的动画效果,时长为 2 秒。
- 帧动画(Frame Animation):帧动画是通过顺序播放一系列图片来实现动画效果,就像播放电影胶片一样。需要先将一系列图片放在
drawable目录下,然后在 XML 文件中定义帧动画:
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item android:drawable="@drawable/frame1" android:duration="100" />
<item android:drawable="@drawable/frame2" android:duration="100" />
<!-- 更多帧 -->
</animation-list>
在 Java 代码中获取到这个AnimationDrawable并启动动画。
说明 padding 和 margin 的区别
- padding:是指 View 内部内容与 View 边界之间的距离,用于控制 View 内部元素的显示位置。比如一个按钮,设置了 padding 后,按钮上的文字或图标会与按钮的边框保持一定的距离。通过设置
android:paddingLeft、android:paddingTop、android:paddingRight、android:paddingBottom分别可以设置 View 左、上、右、下四个方向的内边距,也可以使用android:padding一次性设置四个方向相同的内边距。它是用于调整 View 自身内部的布局空间,不会影响到其他 View 的位置。 - margin:是指 View 与周围其他 View 之间的距离,用于控制 View 在父布局中的位置以及与其他 View 之间的间距。例如在一个线性布局中,设置一个按钮的 margin,会使这个按钮与相邻的其他按钮或视图之间产生一定的间隔。通过
android:marginLeft、android:marginTop、android:marginRight、android:marginBottom分别设置 View 左、上、右、下四个方向的外边距,同样也可以使用android:margin来统一设置。它主要是用于调整 View 与外部其他 View 之间的空间关系,会影响到 View 在布局中的整体位置和布局效果。
简单来说,padding 是 View 内部的空间调整,而 margin 是 View 与外部其他 View 之间的空间调整。
说说安卓 Activity 生命周期
Android 中 Activity 的生命周期由一系列的回调方法组成,主要包括以下几个关键阶段:
- 启动阶段:当 Activity 第一次被创建时,会依次调用
onCreate()、onStart()、onResume()方法。onCreate()方法用于进行 Activity 的初始化操作,如设置布局、初始化变量等;onStart()方法表示 Activity 正在被启动,即将可见,但还未完全显示在前台;onResume()方法表示 Activity 已经可见并获取了焦点,开始与用户进行交互。 - 运行阶段:当 Activity 处于前台运行状态时,它可以响应用户的操作。此时如果有新的 Activity 启动或者用户按下 Home 键等情况,Activity 会进入暂停或停止状态。
- 暂停阶段:当 Activity 失去焦点但仍然可见时,会调用
onPause()方法。比如弹出一个对话框覆盖了部分 Activity 界面,此时该 Activity 就处于暂停状态。在onPause()方法中,可以进行一些轻量级的资源释放等操作,但不能进行耗时操作,因为下一个 Activity 的启动可能会等待onPause()执行完成。 - 停止阶段:当 Activity 完全不可见时,会调用
onStop()方法。此时 Activity 可能被其他 Activity 完全覆盖或者用户切换到了其他应用。在onStop()方法中,可以进行一些更重量级的资源释放操作,但如果 Activity 要被销毁,系统可能会直接跳过onStop()调用onDestroy()。 - 销毁阶段:当 Activity 被销毁时,会调用
onDestroy()方法。这是 Activity 生命周期的最后一个方法,用于进行资源的彻底释放,如关闭数据库连接、取消注册的广播接收器等。 另外,当 Activity 从不可见状态变为可见状态时,会调用onRestart()方法,然后再依次调用onStart()、onResume()方法回到运行状态。理解 Activity 的生命周期对于合理管理资源、保证应用的稳定性和用户体验至关重要。
介绍安卓有哪些动画,属性动画的用法
- 安卓动画类型
- 帧动画:通过顺序播放一系列预先定义好的图片帧来创建动画效果,就像播放幻灯片一样,适用于表现复杂且不规则的动画,如游戏中的角色动作。
- 补间动画:只需要定义动画的起始和结束状态,中间的过渡过程由系统自动计算生成,包括平移、旋转、缩放和透明度变化等,优点是实现简单,但只能对 View 进行操作,无法改变 View 的实际属性。
- 属性动画:可以对任何对象的属性进行动画操作,不仅限于 View,它可以改变对象的实际属性值,动画效果更加灵活和丰富。
- 属性动画的用法
- 使用 ValueAnimator:ValueAnimator 是属性动画的核心类之一,用于计算动画的过渡值。可以通过 ofInt、ofFloat、ofObject 等方法来指定动画的起始值和结束值。例如,
ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);创建了一个从 0 到 1 的浮点型动画。然后设置动画的持续时间、插值器等属性,最后通过 addUpdateListener 监听动画的更新,在回调中更新对象的属性。 - 使用 ObjectAnimator:ObjectAnimator 是 ValueAnimator 的子类,它可以直接对对象的属性进行动画操作。比如,
ObjectAnimator anim = ObjectAnimator.ofFloat(view, "alpha", 0f, 1f);可以实现对一个 View 的透明度从 0 到 1 的动画效果,第一个参数是要操作的对象,第二个参数是属性名,后面是起始值和结束值。 - 组合动画:可以通过 AnimatorSet 将多个属性动画组合在一起,实现更复杂的动画效果。例如,同时实现一个 View 的平移和旋转动画。
- 使用 ValueAnimator:ValueAnimator 是属性动画的核心类之一,用于计算动画的过渡值。可以通过 ofInt、ofFloat、ofObject 等方法来指定动画的起始值和结束值。例如,
项目中使用的动画,帧动画时间间隔的设定,帧动画对图片大小的要求
- 项目中常见的动画应用场景
- 引导页动画:使用帧动画展示一系列精美的引导图片,配合补间动画或属性动画实现页面的切换和元素的显示隐藏,给用户带来直观的引导体验。
- 加载动画:属性动画可以用于实现加载进度条的动态效果,通过不断变化的进度条长度或旋转的加载图标,让用户了解加载状态。
- 交互反馈动画:当用户点击按钮时,通过补间动画或属性动画实现按钮的缩放、变色等效果,给予用户操作反馈,增强交互性。
- 帧动画时间间隔的设定
- 在 Android 中,通过在 XML 文件中定义帧动画时,可以使用
<item>标签的android:duration属性来设置每一帧的显示时间间隔。例如:
- 在 Android 中,通过在 XML 文件中定义帧动画时,可以使用
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item android:drawable="@drawable/frame1" android:duration="100"/>
<item android:drawable="@drawable/frame2" android:duration="100"/>
<!-- 其他帧 -->
</animation-list>
这里每一帧的时间间隔设置为 100 毫秒。也可以在 Java 代码中通过AnimationDrawable的setOneShot()和start()方法来控制帧动画的播放。
- 帧动画对图片大小的要求
- 一般来说,没有严格固定的图片大小要求,但为了保证动画的流畅性和性能,建议图片大小不要过大。如果图片过大,会占用过多的内存,可能导致应用卡顿甚至 OOM(OutOfMemory)错误。通常,根据不同的设备分辨率和屏幕尺寸,将图片的尺寸控制在合理范围内。对于普通的手机应用,单张图片的分辨率在几百像素乘以几百像素左右较为合适,同时要注意图片的格式,尽量使用 PNG 等压缩比高且支持透明通道的格式,以减少内存占用。
介绍自定义 view 相关内容,包括 view 的几个方法及其作用和顺序,哪个方法会被调用多次
- 自定义 View 的基本流程
- 首先需要继承 View 或其子类,然后重写相关方法来实现自定义的功能和外观。通常会重写构造方法,用于初始化一些属性和资源。
- View 的主要方法及其作用和顺序
- onMeasure():用于测量 View 的大小,确定其宽度和高度。在这个方法中,需要调用
setMeasuredDimension()方法来设置测量后的尺寸。 - onLayout():当 View 需要确定其子 View 的位置和大小时会调用此方法,用于布局子 View。
- onDraw():用于绘制 View 的内容,在这里可以使用 Canvas 和 Paint 等对象来绘制图形、文本等。
- 它们的调用顺序通常是先
onMeasure(),然后onLayout(),最后onDraw()。当 View 的大小或布局发生变化时,这几个方法可能会被重新调用。
- onMeasure():用于测量 View 的大小,确定其宽度和高度。在这个方法中,需要调用
- 会被多次调用的方法
onDraw()方法会被多次调用。当 View 的内容需要更新,比如发生了动画效果、数据变化导致需要重新绘制等情况,系统会频繁调用onDraw()方法来保证 View 的显示内容是最新的。而onMeasure()和onLayout()通常在 View 的大小或布局发生改变时才会被调用,相对来说调用频率低于onDraw()。
介绍安卓多线程相关知识
- 线程的基本概念
- 在安卓中,线程是程序执行的最小单元,它可以独立执行一段代码。安卓应用默认运行在主线程(UI 线程)中,负责处理用户界面的绘制和交互等操作。但如果在主线程中执行耗时操作,会导致界面卡顿甚至 ANR(Application Not Responding)错误,所以需要使用多线程来处理耗时任务,如网络请求、文件读取等。
- 安卓多线程的实现方式
- 继承 Thread 类:通过继承 Thread 类,重写
run()方法,在run()方法中编写线程要执行的任务。例如:
- 继承 Thread 类:通过继承 Thread 类,重写
class MyThread extends Thread {
@Override
public void run() {
// 这里是线程执行的代码
}
}
MyThread thread = new MyThread();
thread.start();
- 实现 Runnable 接口:实现 Runnable 接口,重写
run()方法,然后将 Runnable 实例传递给 Thread 类的构造函数来创建线程。例如:
class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行的代码
}
}
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
- 使用 HandlerThread:它是 Thread 的子类,内部创建了一个 Looper,可以方便地处理消息队列。常用于需要在子线程中处理消息的场景。
- 使用 AsyncTask:是安卓提供的一个轻量级的异步任务类,用于在后台执行任务并在主线程更新 UI,它封装了线程池和 Handler 的操作,使用起来比较简单。
- 线程间通信
- Handler:通过在主线程中创建 Handler 对象,在子线程中发送消息,主线程中的 Handler 可以接收到消息并在
handleMessage()方法中处理,从而实现子线程与主线程的通信。 - MessageQueue 和 Looper:MessageQueue 用于存储消息,Looper 用于循环处理 MessageQueue 中的消息,它们配合 Handler 实现线程间的消息传递。
- 共享变量和锁:多个线程可以通过共享变量来进行通信,但需要注意线程安全问题,通常使用
synchronized关键字或 Lock 来保证数据的一致性。
- Handler:通过在主线程中创建 Handler 对象,在子线程中发送消息,主线程中的 Handler 可以接收到消息并在
说明 Broadcast 有几种类型,解释粘性广播和有序广播
- Broadcast 的类型
- 普通广播:是一种完全异步的广播,所有注册了相应广播接收器的组件都可以接收到广播,它们之间没有先后顺序,相互独立。这种广播的优点是传播速度快,效率高,但无法保证接收的顺序和对广播进行拦截等操作。
- 有序广播:广播按照优先级顺序依次发送给各个广播接收器,优先级高的接收器可以先接收到广播,并可以对广播进行处理,如修改广播内容或截断广播,阻止其继续传播。
- 粘性广播:粘性广播发送后会一直存在于系统中,直到有相应的接收器处理了该广播或者被移除。新注册的符合条件的广播接收器可以立即接收到该粘性广播,即使它是在广播发送之后才注册的。
- 本地广播:是应用内的广播,只能在应用内部发送和接收,不会泄漏到应用外部,安全性较高,主要用于应用内不同组件之间的通信。
- 粘性广播
- 粘性广播在某些特定场景下很有用,比如系统的电池电量变化广播,如果使用粘性广播,当有新的与电池电量相关的功能模块启动时,它可以立即获取到当前的电池电量状态,而不需要等待下一次电池电量变化事件的发生。但由于粘性广播会一直驻留在系统中,可能会导致内存泄漏等问题,所以在 Android 5.0(API 级别 21)及以上版本中,已经不推荐使用粘性广播,并且应用需要具有特定的权限才能发送粘性广播。
- 有序广播
- 有序广播可以根据广播接收器的优先级来控制广播的接收顺序,在一些需要对广播进行拦截或按顺序处理的场景中非常有用。例如,在一个应用中有多个模块都需要处理网络连接变化的广播,优先级高的模块可以先检查网络状态,如果网络不可用,就可以截断广播,不让其他模块再进行不必要的网络操作,从而提高应用的稳定性和性能。在注册广播接收器时,可以通过设置
android:priority属性来指定优先级,数值越大,优先级越高。
- 有序广播可以根据广播接收器的优先级来控制广播的接收顺序,在一些需要对广播进行拦截或按顺序处理的场景中非常有用。例如,在一个应用中有多个模块都需要处理网络连接变化的广播,优先级高的模块可以先检查网络状态,如果网络不可用,就可以截断广播,不让其他模块再进行不必要的网络操作,从而提高应用的稳定性和性能。在注册广播接收器时,可以通过设置
阐述 handler 机制
Handler 机制是 Android 中实现线程间通信和消息处理的重要机制,主要由 Handler、Message、MessageQueue 和 Looper 四个部分组成。
- Handler:用于发送消息和处理消息。可以通过
sendMessage()等方法将 Message 发送到 MessageQueue 中,同时在handleMessage()方法中处理接收到的消息。 - Message:消息对象,用于携带数据和信息。可以通过
what、arg1、arg2等字段来传递简单数据,也可以通过obj字段传递复杂对象。 - MessageQueue:消息队列,用于存储 Handler 发送过来的 Message。它采用先进先出的原则,Looper 会不断从 MessageQueue 中取出 Message 并交给对应的 Handler 处理。
- Looper:每个线程只能有一个 Looper,它会不断循环从 MessageQueue 中取出 Message 并分发到对应的 Handler。在主线程中,系统已经自动为我们创建了 Looper,而在子线程中如果需要使用 Handler 机制,则需要手动创建 Looper。
Handler 机制的工作流程是:首先在主线程或子线程中创建 Handler 对象,并重写handleMessage()方法来处理消息。然后在需要发送消息的地方,通过 Handler 的sendMessage()等方法将 Message 发送到 MessageQueue 中。Looper 不断从 MessageQueue 中取出 Message,根据 Message 中的 target 字段找到对应的 Handler,然后调用 Handler 的handleMessage()方法来处理消息。
分析 handler 内存泄漏与 Looper 的关系,以及 Message 为什么没有被回收
在 Android 中,如果 Handler 持有了外部类的引用,而外部类又引用了一些资源或其他对象,当 Handler 所在的线程的 Looper 还在运行时,即使外部类已经没有其他地方引用,也无法被回收,从而导致内存泄漏。这是因为 MessageQueue 中还持有 Message,而 Message 又持有 Handler 的引用,Handler 又持有外部类的引用,形成了一个引用链,使得垃圾回收器无法回收外部类及其相关资源。
对于 Message 没有被回收,主要是因为 Message 被存放在 MessageQueue 中,只要 MessageQueue 还在,并且没有被处理完,Message 就不会被回收。另外,Message 在使用过程中可能会被放入 Message 池,以便重复利用,这也导致它在一定时间内不会被回收。
为了避免 Handler 内存泄漏,可以采用弱引用的方式来持有外部类的引用,或者在不需要使用 Handler 时,及时调用removeCallbacksAndMessages()方法来清除 MessageQueue 中的消息和回调。
Handler 如何实现子线程和主线程通信
Handler 可以在子线程和主线程之间进行通信,主要是通过在不同线程中创建 Handler 并发送和接收消息来实现。
- 在主线程中创建 Handler:在主线程中可以直接创建 Handler 对象,并重写
handleMessage()方法来处理消息。由于主线程默认已经有 Looper,所以不需要手动创建。 - 在子线程中发送消息:在子线程中获取主线程的 Handler 对象,然后通过
sendMessage()等方法将 Message 发送到主线程的 MessageQueue 中。 - 主线程处理消息:主线程的 Looper 会不断从 MessageQueue 中取出 Message,并交给对应的 Handler 的
handleMessage()方法处理,从而实现了子线程向主线程发送消息并在主线程中处理的功能。 - 主线程向子线程发送消息:同样的道理,也可以在子线程中创建 Handler 并启动 Looper,然后在主线程中获取子线程的 Handler 对象,通过发送消息来实现主线程向子线程发送数据和指令等操作。
说明 asynctask 原理
AsyncTask 是 Android 提供的一个轻量级的异步任务类,用于在后台执行任务,并在主线程更新 UI。它的原理主要基于线程池和 Handler 机制。
- 核心类和接口:AsyncTask 内部包含了几个核心的类和接口,如
AsyncTaskLoader、LoaderManager等。它通过泛型来指定参数、进度和结果的类型。 - 任务执行流程:当调用
execute()方法时,AsyncTask 会将任务提交到线程池中,在后台线程执行doInBackground()方法来完成耗时操作。在执行过程中,可以通过publishProgress()方法来更新任务进度,这个方法会调用onProgressUpdate()方法在主线程更新 UI。当任务执行完成后,会调用onPostExecute()方法在主线程处理结果。 - 线程池管理:AsyncTask 使用了一个线程池来管理后台任务的执行,默认情况下,它会根据系统资源情况来创建合适数量的线程。这样可以避免过多的线程创建和销毁,提高性能和效率。
介绍 fragment 生命周期
Fragment 的生命周期与 Activity 有一定的关联,但也有自己的特点,主要包括以下几个状态和方法:
- onAttach():当 Fragment 与 Activity 建立关联时调用,此时可以获取到 Activity 的引用。
- onCreate():在 Fragment 创建时调用,用于初始化一些数据和资源。
- onCreateView():用于创建 Fragment 的视图布局,在这里可以通过 LayoutInflater 来加载布局文件,并返回视图对象。
- onViewCreated():在视图创建完成后调用,此时可以对视图进行进一步的操作和初始化。
- onActivityCreated():当关联的 Activity 的
onCreate()方法执行完成后调用,此时可以访问 Activity 中的视图和数据。 - onStart():在 Fragment 可见之前调用,此时 Fragment 已经与 Activity 建立关联,并且视图已经创建。
- onResume():当 Fragment 可见并可交互时调用,此时可以进行一些与用户交互相关的操作。
- onPause():当 Fragment 失去焦点但仍然可见时调用,用于保存一些临时数据和暂停一些正在进行的操作。
- onStop():当 Fragment 不可见时调用,此时可以释放一些资源和停止一些后台任务。
- onDestroyView():在 Fragment 的视图被销毁时调用,用于释放与视图相关的资源。
- onDestroy():在 Fragment 被销毁时调用,用于释放所有资源和进行清理操作。
- onDetach():当 Fragment 与 Activity 解除关联时调用,此时可以进行一些最后的清理工作。
介绍 android 四大组件
Android 四大组件是 Activity(活动)、Service(服务)、Broadcast Receiver(广播接收器)和 Content Provider(内容提供者)。
- Activity:是 Android 中最基本的组件,用于实现用户界面。一个 Activity 通常对应一个屏幕的内容,用户可以在其中进行各种交互操作,如点击按钮、输入文本等。它有自己的生命周期,包括 onCreate、onStart、onResume 等方法,开发者可以根据需求在这些方法中进行相应的逻辑处理,如初始化界面、加载数据等。
- Service:用于在后台执行长时间运行的操作,不提供用户界面。比如音乐播放服务、文件下载服务等。Service 分为两种类型,一种是启动服务(Started Service),通过 startService 方法启动,会在后台一直运行,直到被停止;另一种是绑定服务(Bound Service),通过 bindService 方法绑定,与调用者有连接关系,当调用者解除绑定时,服务可能会被销毁。
- Broadcast Receiver:用于接收系统或应用发出的广播消息,比如电池电量变化、网络连接变化等广播。可以通过注册静态广播接收器(在 AndroidManifest.xml 文件中注册)或动态广播接收器(在代码中通过 registerReceiver 方法注册)来接收广播。当接收到广播时,会调用 onReceive 方法来处理广播事件。
- Content Provider:用于在不同的应用之间共享数据,比如联系人数据、短信数据等。其他应用可以通过 ContentResolver 来访问 Content Provider 提供的数据,实现了数据的封装和安全共享。它提供了增删改查等操作方法,方便其他应用对数据进行操作。
使用过 Fragment 吗?Activity 如何传参给 Fragment?为什么用 setArgument 传参,而不是使用带有参数的构造器?
在 Android 开发中经常会使用 Fragment。Activity 传参给 Fragment 主要有以下方式:
- 使用 setArguments 方法:可以在 Activity 中创建 Fragment 时,通过 Bundle 来传递参数。例如:
MyFragment fragment = new MyFragment();
Bundle bundle = new Bundle();
bundle.putString("key", "value");
fragment.setArguments(bundle);
- 在 Fragment 中定义方法:在 Fragment 中定义公开的方法,在 Activity 中获取到 Fragment 实例后,调用该方法传递参数。
使用 setArguments 传参而不使用带有参数的构造器,主要有以下原因:
- Fragment 的重建:当 Activity 由于配置变化(如屏幕旋转)而重新创建时,Fragment 也会被重新创建。使用 setArguments 方法传递的参数会被系统自动保存和恢复,而通过构造器传递的参数则不会。
- FragmentManager 的管理:FragmentManager 在管理 Fragment 时,会使用默认的无参构造器来创建 Fragment。如果使用带参数的构造器,可能会导致 FragmentManager 无法正确创建 Fragment。
- 代码的可读性和可维护性:使用 setArguments 方法可以更清晰地看到传递给 Fragment 的参数,并且可以在 Fragment 的 onCreate 等方法中方便地获取参数,使代码更易于理解和维护。
知道系统杀进程吗?如果栈中从底到顶是 A,B,C,系统杀了应用后重新点击,显示哪个 Activity?此时 C 里有 Fragment,如何恢复?栈下面的 A,B 是否存在?
系统杀进程是为了回收内存等资源,当系统内存不足或者应用处于后台等情况时,可能会被系统杀死。 如果栈中从底到顶是 A、B、C,系统杀了应用后重新点击,通常会显示 A Activity。这是因为系统杀死应用后,Activity 栈被清空,重新启动应用会从任务栈的底部开始启动,也就是启动 A。 当 C 里有 Fragment 时,恢复方式如下:可以在 Fragment 中重写 onSaveInstanceState 方法,在这个方法中保存 Fragment 的状态数据。当 Activity 重新创建后,在 Fragment 的 onCreate 或 onViewCreated 等方法中,通过获取保存的状态数据来恢复 Fragment 的状态。 一般情况下,系统杀了应用后,栈下面的 A、B 不存在了,因为整个任务栈都被清空。但是如果应用采用了一些特殊的技术,如使用了进程保活技术或者将 Activity 设置为具有特定的启动模式和属性,可能会有不同的情况。比如将 Activity 设置为 singleInstance 模式,可能会在一定程度上保留 Activity 的实例,但这需要根据具体的设置和场景来确定。
介绍 Android 的持久化方式,ContentProvider 自身是否存储数据?
Android 的持久化方式主要有以下几种:
- SharedPreferences:用于存储简单的键值对数据,如用户的配置信息、登录状态等。可以通过 getSharedPreferences 方法获取 SharedPreferences 实例,然后使用 putXXX 和 getXXX 方法来存储和获取数据。
- 文件存储:可以将数据存储到文件中,分为内部存储和外部存储。内部存储通过 Context 提供的 openFileInput 和 openFileOutput 等方法进行操作,数据存储在应用的私有目录下;外部存储则需要申请相应的权限,可以使用 Environment.getExternalStorageDirectory 等方法来获取外部存储路径,进行文件的读写操作。
- SQLite 数据库:用于存储结构化数据,通过 SQLiteOpenHelper 类来创建和管理数据库,使用 SQL 语句进行数据的增删改查操作。
- Room 数据库:是 Google 推荐的 Android 数据库框架,它在 SQLite 的基础上进行了封装,提供了更简洁的 API 和更好的性能,支持数据的持久化、缓存等功能。
ContentProvider 自身通常并不直接存储数据,它主要是作为一个数据共享的接口,用于在不同的应用之间共享数据。它可以包装底层的数据源,如 SQLite 数据库、文件等,将数据以统一的接口提供给其他应用访问。其他应用通过 ContentResolver 来与 ContentProvider 进行交互,实现数据的查询、插入、更新和删除等操作。
说明 Service 的保活方法
以下是一些常见的 Service 保活方法:
- 提升 Service 的优先级
- 设置前台 Service:将 Service 设置为前台 Service 可以使其具有较高的优先级,不容易被系统杀死。通过 startForeground 方法将 Service 设置为前台 Service,并传入一个通知。
- 使用粘性 Service:在 Service 的 onStartCommand 方法中返回 START_STICKY,当 Service 被系统杀死后,系统会在资源允许的情况下尝试重新创建 Service。
- 利用系统广播
- 注册静态广播接收器:在 AndroidManifest.xml 文件中注册一些系统广播的接收器,如网络变化广播、电池电量变化广播等。当接收到这些广播时,可以在广播接收器中启动 Service,确保 Service 在合适的时机被重新启动。
- 监听屏幕解锁广播:监听屏幕解锁广播,当用户解锁屏幕时,判断 Service 是否已经停止,如果停止则重新启动 Service。
- 双进程守护
- 创建两个 Service:一个主 Service 和一个守护 Service,主 Service 用于执行具体的业务逻辑,守护 Service 用于监控主 Service 的状态。当守护 Service 发现主 Service 被杀死时,重新启动主 Service;同理,主 Service 也可以监控守护 Service 的状态,互相守护。
- JobScheduler
- 使用 JobScheduler:可以通过 JobScheduler 来安排 Service 的执行,它可以根据系统的资源情况和网络状态等条件来执行任务。当满足设定的条件时,JobScheduler 会启动 Service,这样可以在一定程度上保证 Service 在合适的时机运行,并且减少被系统杀死的可能性。
使用过 Service 吗?说明其生命周期,当内存不足时 Service 被杀死,如何重启这个 Service?
Android 中的 Service 是一种可在后台执行长时间运行操作而不提供用户界面的组件。
- Service 的生命周期
- startService 启动方式:当通过 startService () 方法启动 Service 时,会依次调用 onCreate ()、onStartCommand () 方法。如果 Service 已经创建,则只会调用 onStartCommand ()。Service 会一直运行,直到通过 stopService () 方法或者在 Service 内部调用 stopSelf () 方法停止,此时会调用 onDestroy () 方法。
- bindService 启动方式:使用 bindService () 方法启动时,会调用 onCreate ()、onBind () 方法,客户端通过 onBind () 方法返回的 IBinder 对象与 Service 进行通信。当所有客户端都解除绑定后,会调用 onUnbind () 和 onDestroy () 方法。
- Service 被杀死后的重启方法
- 在 onStartCommand () 方法中返回合适的值:可以在 Service 的 onStartCommand () 方法中返回 START_STICKY 或 START_REDELIVER_INTENT。START_STICKY 表示如果 Service 被系统杀死,在资源允许的情况下会重新创建并执行 onStartCommand () 方法,但不会重新传递 Intent;START_REDELIVER_INTENT 则会重新传递 Intent。
- 使用 JobScheduler:可以结合 JobScheduler 来安排 Service 的重启,在 JobInfo.Builder 中设置相关条件和任务,当满足条件时 JobScheduler 会触发任务,从而重启 Service。
- 使用 AlarmManager:通过 AlarmManager 设置定时任务,当到达设定时间时,触发一个 PendingIntent,这个 PendingIntent 可以启动 Service,从而实现 Service 的重启。
说明动态权限申请,说说哪些是危险权限,举出 5 个例子。
在 Android 系统中,从 Android 6.0(API 级别 23)开始引入了动态权限申请机制,以更好地保护用户隐私和安全。
- 动态权限申请步骤
- 检查权限:首先使用 ContextCompat.checkSelfPermission () 方法检查应用是否已经拥有所需权限。如果返回 PackageManager.PERMISSION_GRANTED,表示已经有权限,否则需要进行申请。
- 请求权限:使用 ActivityCompat.requestPermissions () 方法来请求权限,该方法接收当前 Activity 实例、要申请的权限数组以及一个请求码作为参数。
- 处理权限结果:在 Activity 的 onRequestPermissionsResult () 方法中处理权限申请的结果,通过请求码判断是哪个权限请求的结果,然后根据权限是否被授予进行相应的操作。
- 危险权限举例
- CAMERA:允许应用访问设备的摄像头,可能会被用于未经用户同意的拍照或录像,从而侵犯用户隐私。
- READ_CONTACTS:可以读取用户的联系人信息,若被恶意应用获取,可能导致用户联系人数据泄露,被用于诈骗等非法活动。
- RECORD_AUDIO:允许应用录制音频,可能会在用户不知情的情况下录制用户的谈话内容,造成隐私泄露。
- WRITE_EXTERNAL_STORAGE:具有向外部存储写入数据的权限,可能会导致用户存储在外部存储中的数据被恶意篡改或删除。
- ACCESS_FINE_LOCATION:能够获取用户的精确位置信息,若被滥用,可能会让用户的行踪被追踪,带来安全隐患。
谈谈对 Android 适配的了解。
Android 适配是开发 Android 应用过程中的重要环节,旨在确保应用在各种不同的 Android 设备和系统版本上都能正常运行并提供良好的用户体验。
- 屏幕适配
- 不同屏幕尺寸:Android 设备有各种不同的屏幕尺寸,如手机、平板等。为了适配不同尺寸的屏幕,开发者可以使用相对布局、线性布局等灵活的布局方式,让界面元素根据屏幕大小自动调整位置和大小。还可以使用限定符,如 values-sw600dp、values-sw720dp 等,为不同屏幕宽度的设备提供不同的布局和资源。
- 不同屏幕密度:屏幕密度指的是每英寸屏幕上的像素数量,常见的有 mdpi、hdpi、xhdpi 等。开发者需要为不同屏幕密度提供相应的图片资源,放在不同的 drawable 目录下,系统会根据设备的屏幕密度自动加载合适的图片,以保证图片的清晰度和显示效果。
- 系统版本适配
- API 版本差异:随着 Android 系统的不断更新,每个版本都可能引入新的 API 和特性,同时也可能对旧的 API 进行修改或废弃。开发者需要根据目标设备的 Android 系统版本,合理地使用 API。对于新的 API,可以使用版本判断来在高版本系统上使用新特性,在低版本系统上采用兼容的方式实现功能。
- 系统行为变化:不同的 Android 系统版本可能会有不同的系统行为和特性,例如权限管理、通知样式等。开发者需要了解这些变化,并在应用中进行相应的适配,以确保应用在不同系统版本上的功能和用户体验一致。
列举用过的第三方库,说明 Okhttp 的优点。
在 Android 开发中,有许多常用的第三方库,比如用于网络请求的 Okhttp、用于图片加载的 Glide、用于依赖注入的 Dagger 等。
Okhttp 是一款优秀的 HTTP 客户端库,具有以下优点:
- 高效性
- 连接池复用:Okhttp 能够复用连接,避免了每次请求都重新建立连接的开销,大大提高了网络请求的效率。例如,在连续发送多个请求到同一个服务器时,它会将连接保存在连接池中,后续请求可以直接使用已有的连接,减少了连接建立的时间和资源消耗。
- GZIP 压缩:自动对请求和响应进行 GZIP 压缩,减少了数据传输量,加快了数据的传输速度。在网络状况不佳的情况下,这种压缩可以显著提高数据的传输效率,节省用户的流量和时间。
- 可靠性
- 自动重连:当网络连接出现问题时,Okhttp 能够自动进行重连操作,提高了请求的成功率。它会根据一定的策略和条件,在连接失败后尝试重新连接,直到请求成功或者达到最大重连次数。
- 请求失败重试:对于因各种原因导致的请求失败,如服务器故障、网络波动等,Okhttp 可以根据设置的重试策略自动重试请求,确保数据能够成功发送和接收。
- 灵活性
- 拦截器机制:通过拦截器,开发者可以方便地对请求和响应进行拦截和处理,比如添加请求头、修改请求参数、打印日志等。可以自定义拦截器来实现各种功能,满足不同的业务需求。
- 支持多种协议:除了支持 HTTP 和 HTTPS 协议外,还支持 SPDY、HTTP/2 等协议,能够适应不同的网络环境和服务器要求,为开发者提供了更多的选择和灵活性。
分析 View 的测绘流程。
View 的测绘流程是 Android 视图系统中非常重要的一部分,它决定了 View 的大小和位置。整个流程主要包括三个方法:measure ()、layout () 和 draw ()。
- measure 过程
- 这是测绘流程的第一步,主要用于测量 View 的大小。在这个过程中,父 View 会调用子 View 的 measure () 方法,传递测量规格(MeasureSpec),这个测量规格包含了测量模式和尺寸大小等信息。测量模式有三种:EXACTLY、AT_MOST 和 UNSPECIFIED。
- 对于自定义 View,如果没有重写 onMeasure () 方法,可能会导致测量不准确。在 onMeasure () 方法中,需要根据测量模式和自身的需求来计算 View 的宽高,并通过 setMeasuredDimension () 方法设置测量后的宽高。
- 例如,对于一个 TextView,如果设置了固定的宽度和高度,那么测量模式就是 EXACTLY,其大小就是设置的固定值;如果设置为 wrap_content,那么测量模式可能是 AT_MOST,TextView 会根据内容的大小来确定自己的实际大小。
- layout 过程
- 当 measure 过程完成后,就进入 layout 过程。这个过程主要用于确定 View 在父容器中的位置。父 View 会调用子 View 的 layout () 方法,传递子 View 的左上角和右下角坐标,从而确定子 View 的位置。
- 在自定义 View 中,如果需要自定义布局位置,可以重写 onLayout () 方法。在 onLayout () 方法中,根据子 View 的测量大小和布局规则,计算并设置子 View 的位置。
- 例如,在一个 LinearLayout 中,它会根据子 View 的排列方向和权重等属性,计算每个子 View 的位置,然后调用子 View 的 layout () 方法来设置其位置。
- draw 过程
- draw 过程是最后一步,用于将 View 绘制到屏幕上。在这个过程中,View 会按照一定的顺序绘制自己的背景、内容和边框等。首先会绘制背景,然后调用 onDraw () 方法绘制内容,最后绘制边框等其他装饰。
- 对于自定义 View,通常需要重写 onDraw () 方法来实现自己的绘制逻辑,比如绘制图形、文字等。在 onDraw () 方法中,可以使用 Canvas 和 Paint 等类来进行绘制操作。
- 例如,在一个自定义的圆形 View 中,在 onDraw () 方法中可以使用 Canvas 的 drawCircle () 方法来绘制圆形,通过设置 Paint 的颜色、样式等属性来实现不同的绘制效果。
由 View 的绘制流程拓展到自定义 View,如果要自定义一个流式标签布局,会设计哪些内容暴露给外界,以及会在 View 的三个方法(onDraw、onLayout、onMeasure)里做哪些相关的工作?
- 暴露给外界的内容
- 标签文本内容:允许外界传入标签显示的文字信息,方便用户根据需求定制。
- 标签文本颜色:提供设置文本颜色的接口,使布局中的标签文本能有不同颜色展示。
- 标签背景颜色:支持外界设置标签的背景色,满足多样化的视觉需求。
- 标签间距:包括水平和垂直方向的间距,方便用户调整标签之间的距离。
- 标签最大宽度:让用户可以限制标签的最大宽度,以适应不同的布局场景。
- onDraw 方法:主要负责绘制标签的外观,如绘制标签的背景形状、文本等。可以根据设置的背景颜色绘制矩形或其他形状的背景,然后使用 Canvas 的 drawText 方法将标签文本绘制到合适的位置,还可以绘制一些边框或装饰元素。
- onLayout 方法:确定每个标签在流式布局中的位置。需要根据标签的数量、宽度、高度以及设置的间距等,计算每个标签应该放置的坐标位置,使标签能够按照流式排列,当一行放不下时自动换行。
- onMeasure 方法:测量标签布局的大小以及每个标签的大小。要遍历所有标签,测量每个标签的宽度和高度,根据标签的最大宽度和布局的宽度限制,计算出需要的行数和列数,从而确定整个流式标签布局的宽度和高度,为后续的布局和绘制提供基础。
讲一些 Android 手势事件处理
Android 中的手势事件处理主要通过 GestureDetector 和 OnTouchListener 来实现。
- GestureDetector:通常用于识别一些常见的手势,如单击、双击、长按、滑动等。首先需要创建一个 GestureDetector 对象,并实现 OnGestureListener 或 OnDoubleTapListener 接口来处理不同的手势事件。例如,onSingleTapUp 方法用于处理单击事件,onDoubleTap 方法用于处理双击事件,onScroll 方法用于处理滑动事件等。
- OnTouchListener:用于监听视图的触摸事件。通过重写 onTouch 方法,可以获取到触摸事件的详细信息,如触摸的坐标、动作类型等。可以根据这些信息来实现自定义的手势处理逻辑。比如,通过判断触摸点的移动距离和速度来识别手势是快速滑动还是缓慢拖动。
- 手势事件的传递:在 Android 中,手势事件会从父视图传递到子视图。如果子视图没有处理某个手势事件,它会向上传递给父视图,直到有视图处理该事件或者到达顶层视图。可以通过调用 requestDisallowInterceptTouchEvent 方法来控制手势事件的传递,决定是否允许父视图拦截子视图的手势事件。
如果要设计一个双击的监听 listener,会怎么设计?
- 使用 GestureDetector:创建一个 GestureDetector 对象,并实现 OnDoubleTapListener 接口。在 onDoubleTap 方法中编写双击事件发生时要执行的逻辑代码。例如:
GestureDetector gestureDetector = new GestureDetector(context, new GestureDetector.OnDoubleTapListener() {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
return false;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
// 这里写双击后的操作逻辑
return true;
}
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
return false;
}
});
// 在视图的onTouch方法中调用
public boolean onTouch(View v, MotionEvent event) {
return gestureDetector.onTouchEvent(event);
}
- 自定义双击监听接口:创建一个自定义的双击监听接口,包含一个双击事件的回调方法。在需要监听双击的视图中,定义一个该接口的实例,并提供一个设置监听器的方法。在视图的触摸事件处理中,通过记录触摸时间和动作来判断是否为双击。当满足双击条件时,调用监听器的回调方法。
讲一下自己处理过的比较复杂的手势处理
在一个图像编辑应用中,需要实现对图片的多种手势操作,如缩放、旋转、平移等。
- 缩放处理:通过记录两个手指触摸点之间的距离变化来实现图片的缩放。在 onTouch 方法中,当检测到 ACTION_POINTER_DOWN 事件时,记录两个手指的坐标,计算初始距离。在后续的 ACTION_MOVE 事件中,再次计算手指间的距离,根据距离变化比例来缩放图片,通过 Matrix 类的 postScale 方法实现。
- 旋转处理:根据两个手指触摸点的角度变化来旋转图片。在 ACTION_POINTER_DOWN 时,记录两个手指的坐标,计算初始角度。在 ACTION_MOVE 时,重新计算角度,根据角度差来旋转图片,使用 Matrix 类的 postRotate 方法。
- 平移处理:当只有一个手指触摸并移动时,实现图片的平移。在 ACTION_DOWN 时,记录手指的初始坐标,在 ACTION_MOVE 时,计算手指移动的距离,通过 Matrix 类的 postTranslate 方法来平移图片。
为了使这些操作更加流畅和准确,还需要进行一些边界处理和精度优化,比如限制图片的最大缩放比例,防止图片超出屏幕范围等。
如果让自己做一个像 ScrollView 那样的 View,怎么设计?
- 测量布局:在 onMeasure 方法中,首先测量子视图的大小,然后根据子视图的总高度和自身的高度限制,确定是否需要滚动。如果子视图的高度超过了自身的高度,需要设置合适的测量模式和尺寸,以便后续能够正确地显示可滚动区域。
- 布局子视图:在 onLayout 方法中,根据测量的结果,将子视图放置在合适的位置。如果是垂直滚动的 ScrollView,需要按照子视图的顺序,从上到下依次布局,确保每个子视图都能正确显示,并且在滚动时能够正确地显示相应的部分。
- 处理滚动事件:通过重写 onTouchEvent 方法来处理触摸事件,实现滚动功能。当手指按下时,记录起始位置;当手指移动时,根据移动的距离计算滚动的偏移量,并通过 scrollTo 或 scrollBy 方法来滚动视图内容。当手指抬起时,根据滚动的速度和惯性,实现平滑的滚动效果,可以使用 Scroller 类来辅助实现。
- 提供滚动监听接口:设计一个滚动监听接口,让外部可以监听 ScrollView 的滚动状态,如滚动的距离、是否到达顶部或底部等。在滚动过程中,通过回调接口方法,将滚动信息传递给外部,以便外部可以根据滚动状态进行相应的操作。
针对 Android 的消息机制,可不可以利用它的特性来检测 ANR,讲一下方案
Android 的消息机制主要由 Handler、MessageQueue、Looper 等组成,主线程的 Looper 负责不断从 MessageQueue 中取出消息并交给 Handler 处理。利用这一特性可以通过以下方案检测 ANR:
- 原理:正常情况下,主线程的消息会被及时处理,如果在一定时间内没有新的消息被处理或者特定消息处理时间过长,就可能发生了 ANR。
- 实现方式:可以通过创建一个子线程,在子线程中每隔一定时间(如 5 秒)向主线程发送一个空消息。在主线程的 Handler 中接收这个消息并记录时间戳。同时,在主线程中开启一个 WatchDog 线程,WatchDog 线程不断检查当前时间与上次接收到消息的时间戳差值。如果差值超过了设定的阈值(如 10 秒),则认为可能发生了 ANR,此时可以记录相关信息,如当前主线程的堆栈信息等,以便后续分析 ANR 原因。
介绍 Activity 之间的通信方法
Activity 之间的通信方法有多种:
- 通过 Intent 传递数据:可以在启动 Activity 时,通过 Intent 的 putExtra 方法传递各种基本数据类型和可序列化、可 Parcelable 的数据。例如,在一个 Activity 中启动另一个 Activity 并传递一个字符串:
Intent intent = new Intent(this, SecondActivity.class);
intent.putExtra("key", "value");
startActivity(intent);
在 SecondActivity 中可以通过 getIntent ().getStringExtra ("key") 获取传递的数据。
- 通过 Bundle 传递数据:将数据先放入 Bundle 中,再将 Bundle 放入 Intent 中传递。适合传递多个数据。
Bundle bundle = new Bundle();
bundle.putString("name", "张三");
bundle.putInt("age", 20);
Intent intent = new Intent(this, SecondActivity.class);
intent.putExtras(bundle);
startActivity(intent);
- 通过静态变量或单例模式:可以在一个类中定义静态变量或使用单例模式来存储数据,在不同的 Activity 中进行访问和修改。但要注意内存管理和并发问题。
- 通过 EventBus 等消息总线框架:注册事件和订阅事件,当一个 Activity 发送事件时,其他订阅了该事件的 Activity 可以接收到并处理。
介绍安卓 AIDL
安卓 AIDL(Android Interface Definition Language)即安卓接口定义语言,是一种用于在不同进程之间进行通信的机制。
- 作用:当应用中的不同组件(如 Service 与 Activity)需要在不同进程间进行通信时,AIDL 可以帮助定义通信接口和数据传输方式。它允许一个进程调用另一个进程中的方法,就好像在本地调用一样,实现了进程间的解耦和通信的规范化。
- 使用步骤:首先创建.aidl 文件,在其中定义接口和方法。例如定义一个 IMyAidlInterface.aidl 文件:
package com.example.aidl;
interface IMyAidlInterface {
int add(int num1, int num2);
}
然后,Android Studio 会自动生成对应的 Java 接口文件。在服务端,实现这个接口并在 Service 中注册。在客户端,通过 ServiceConnection 绑定服务,获取到服务端的代理对象,从而可以调用服务端暴露的方法。
说明 weight 属性的作用
在 Android 布局中,weight 属性主要用于分配子视图在父布局中的空间比例。
- 在 LinearLayout 中的作用:当 LinearLayout 的方向为水平时,weight 属性决定了子视图在水平方向上占据的空间比例。例如,有两个 TextView 在一个水平方向的 LinearLayout 中,一个 TextView 的 weight 设为 1,另一个设为 2,那么第二个 TextView 将占据比第一个 TextView 多一倍的水平空间。垂直方向同理。
- 在 RelativeLayout 等布局中的特殊情况:一般来说 RelativeLayout 等布局不直接依赖 weight 属性来分配空间,但在某些嵌套布局或配合其他属性使用时,也可以间接通过 weight 来影响子视图的布局效果。比如在 RelativeLayout 中嵌套一个 LinearLayout,此时 LinearLayout 中的子视图的 weight 属性依然可以在 LinearLayout 内部起作用来分配空间。
- 与其他属性配合:weight 属性通常与 layout_width 或 layout_height 属性配合使用。当 width 或 height 设为 0dp 时,weight 属性会更精确地按照比例分配空间。如果设为 wrap_content 或 match_parent 等,weight 属性的作用会受到一定影响,空间分配会结合其他因素综合考虑。
动画使用过吗?
在 Android 开发中,动画是非常重要的一部分,常见的动画类型有以下几种:
- 补间动画:通过对 View 的平移、旋转、缩放、透明度等属性进行渐变来实现动画效果。可以在 XML 文件中定义,也可以在代码中动态创建。例如在 XML 中定义一个旋转动画:
<rotate
android:duration="1000"
android:fromDegrees="0"
android:toDegrees="360"
android:pivotX="50%"
android:pivotY="50%"/>
- 帧动画:通过顺序播放一系列图片来实现动画,就像播放电影胶片一样。可以在 XML 中定义帧动画的图片序列和播放时间等属性。
- 属性动画:可以对任何对象的属性进行动画操作,不仅仅局限于 View。它支持更灵活的动画效果,如自定义属性的动画、多属性组合动画等。例如,对一个 View 的背景颜色进行渐变动画:
ObjectAnimator animator = ObjectAnimator.ofArgb(view, "backgroundColor", Color.RED, Color.BLUE);
animator.setDuration(2000);
animator.start();
Recyclerview 中怎么实现分隔符?
在 RecyclerView 中实现分隔符有多种方式。一种常见的方法是使用DividerItemDecoration,它是 RecyclerView 提供的一个内置类。只需创建DividerItemDecoration的实例,并将其添加到 RecyclerView 中即可。示例代码如下:
DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(recyclerView.getContext(),
LinearLayoutManager.VERTICAL);
recyclerView.addItemDecoration(dividerItemDecoration);
还可以自定义ItemDecoration来实现更个性化的分隔符。通过继承RecyclerView.ItemDecoration类,重写onDraw或onDrawOver方法来绘制分隔符。在onDraw方法中,可以使用Canvas和Paint来绘制线条或其他形状作为分隔符。另外,也可以在布局文件中为 RecyclerView 的每个 item 添加一个分隔符视图,通过设置其样式和属性来实现分隔效果。比如在 item 布局文件中添加一个View作为分隔线,并设置其高度、颜色等属性。
讲一下构造方法、静态方法。
构造方法是一种特殊的方法,用于在创建对象时初始化对象的状态。它的名称与类名相同,没有返回值类型,甚至连void也不能有。构造方法可以有参数,通过参数可以在创建对象时为对象的属性赋初始值。如果一个类没有显式地定义构造方法,Java 会自动为其提供一个默认的无参构造方法。当有多个构造方法时,就构成了构造方法的重载,它们可以根据不同的参数列表来执行不同的初始化操作。
静态方法是属于类的方法,而不是属于类的某个对象。它使用static关键字来修饰,可以通过类名直接调用,而不需要创建类的实例。静态方法不能直接访问非静态的成员变量和成员方法,因为非静态成员是属于对象的,而静态方法在调用时可能还没有对象存在。静态方法通常用于执行一些与类相关的通用操作,比如工具类中的方法,像Math.sqrt()就是Math类的静态方法,用于计算平方根。
讲一下 handlerthread 和 thread 的区别。
HandlerThread和Thread都用于在 Android 中实现多线程,但它们有一些重要区别。
Thread是 Java 中最基本的线程类,用于创建一个新的线程来执行特定的任务。它只是简单地提供了一个执行线程的机制,需要手动管理线程的生命周期和执行逻辑。HandlerThread是Thread的子类,它内部创建了一个Looper,可以在该线程中使用Handler来处理消息。HandlerThread在启动后会自动创建一个消息循环,使得可以通过Handler发送和处理消息,方便了线程间的通信和任务的调度。它适用于需要在一个单独的线程中处理消息队列的场景,比如进行文件下载、数据处理等任务,并且可以方便地与主线程或其他线程进行交互。
介绍 looper、handler、thread 之间的关系。
在 Android 中,Looper、Handler和Thread之间存在着紧密的联系。
Thread是执行具体任务的线程,每个线程都可以有自己的Looper。一个线程通过Looper.prepare()方法来为该线程创建一个Looper对象,用于管理该线程的消息队列。Looper负责管理消息队列,它会不断地从消息队列中取出消息,并将消息分发给对应的Handler。一个线程只能有一个Looper,可以通过Looper.myLooper()方法获取当前线程的Looper。Handler用于发送和处理消息。它可以通过sendMessage()等方法将消息发送到Looper管理的消息队列中,同时在Handler中重写handleMessage()方法来处理接收到的消息。Handler在创建时需要与一个Looper关联,这样它才能知道从哪个消息队列中获取消息并进行处理。
说明 Android 的 window 相关知识。
在 Android 中,Window是一个非常重要的概念。它是一个抽象的概念,用于描述应用程序窗口的属性和行为。
- 每个
Activity都有一个对应的Window,它是Activity显示的载体。Window提供了一个区域,用于绘制Activity的内容视图和其他相关的界面元素。 Window的类型有多种,包括应用窗口、系统窗口等。应用窗口用于显示应用的界面,而系统窗口则用于显示系统级的界面元素,如状态栏、导航栏等。WindowManager是用于管理Window的服务,它可以用于添加、更新和删除Window。通过WindowManager,可以设置Window的各种属性,如大小、位置、透明度等。Window的层级关系也很重要,不同的Window可以根据其层级来确定显示的顺序。层级较高的Window会显示在层级较低的Window之上。
介绍 SurfaceView
SurfaceView 是 Android 中用于实现自定义绘图和高性能图形渲染的重要组件。它允许开发者在一个独立的线程中进行绘制操作,而不是在主线程,这对于需要频繁更新界面、进行动画绘制或处理大量图形数据的应用非常有帮助,可以避免阻塞主线程,保证应用的流畅性。
SurfaceView 有自己独立的 Surface,这是一块专门用于绘图的内存区域。当需要绘制内容时,通过 SurfaceHolder 来获取 Surface 的控制权,然后在一个单独的线程中利用 Canvas 进行绘制操作。比如在开发游戏时,游戏中的各种画面元素、角色动画等都可以通过 SurfaceView 来实现。在视频播放应用中,也可以使用 SurfaceView 来显示视频画面,通过 SurfaceView 提供的接口,将视频解码后的数据绘制到 Surface 上。
与普通的 View 相比,SurfaceView 的绘图操作更加灵活,能够实现更复杂的图形效果和更高的帧率。但同时,由于它需要在单独的线程中进行管理和绘制,使用起来相对复杂一些,需要开发者对线程管理和绘图机制有深入的理解。
介绍花式 manager 的相关知识
猜测你想问的是 “介绍 FragmentManager 的相关知识”。FragmentManager 是 Android 中用于管理 Fragment 的重要类。它负责 Fragment 的添加、替换、移除等操作,以及处理 Fragment 之间的事务。
在一个 Activity 中,可以通过 FragmentManager 来动态地添加或替换不同的 Fragment,实现界面的灵活切换和复用。例如,在一个新闻类应用中,主界面可能通过 FragmentManager 来管理不同频道的 Fragment,用户点击不同的频道按钮时,通过 FragmentManager 将对应的 Fragment 显示在界面上。
FragmentManager 通过 FragmentTransaction 来执行具体的 Fragment 操作。可以使用 beginTransaction () 方法开启一个事务,然后使用 add ()、replace ()、remove () 等方法对 Fragment 进行操作,最后通过 commit () 方法提交事务,使操作生效。
此外,FragmentManager 还提供了一些方法来获取当前添加的 Fragment、回退 Fragment 事务等功能。它还支持 Fragment 的嵌套,即一个 Fragment 中可以再包含其他的 Fragment,进一步增强了界面的灵活性和可扩展性。
说明 Activity 如何判断自己是处于前台还是后台
Activity 可以通过多种方式判断自己处于前台还是后台。一种常见的方法是利用 Activity 的生命周期回调方法。当 Activity 进入前台时,会调用 onResume () 方法,而当 Activity 进入后台时,会调用 onPause () 方法和 onStop () 方法。可以在这些方法中设置相应的标志位来记录 Activity 的状态。
例如:
public class MainActivity extends AppCompatActivity {
private boolean isInForeground = false;
@Override
protected void onResume() {
super.onResume();
isInForeground = true;
}
@Override
protected void onPause() {
super.onPause();
isInForeground = false;
}
public boolean isInForeground() {
return isInForeground;
}
}
另外,也可以通过 ActivityManager 来获取当前运行的任务栈信息,判断当前 Activity 是否处于栈顶,从而确定是否在前台。通过 ActivityManager 获取 RunningTaskInfo 列表,查看栈顶的 Activity 是否是当前 Activity。
ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningTaskInfo> runningTasks = activityManager.getRunningTasks(1);
if (!runningTasks.isEmpty()) {
ComponentName topActivity = runningTasks.get(0).topActivity;
if (topActivity.getClassName().equals(MainActivity.this.getClass().getName())) {
// 当前Activity在前台
} else {
// 当前Activity在后台
}
}
说明单例模式在 Android 中的应用,从单例引到 static 的用法,对成员,对方法的作用,包括初始化的过程。构造函数的修饰符在单例中的表现
单例模式在 Android 中应用广泛,比如应用中的全局配置信息管理、数据库操作对象、网络请求管理器等通常会采用单例模式来实现。它确保在整个应用程序中,一个类只有一个实例存在,方便数据的共享和管理。
以数据库操作对象为例,使用单例模式可以保证在整个应用中只有一个数据库连接实例,避免多次创建连接造成资源浪费。
public class DatabaseManager {
private static DatabaseManager instance;
private SQLiteDatabase database;
private DatabaseManager() {
// 初始化数据库操作
database = openDatabase();
}
public static DatabaseManager getInstance() {
if (instance == null) {
instance = new DatabaseManager();
}
return instance;
}
private SQLiteDatabase openDatabase() {
// 打开或创建数据库的代码
return SQLiteDatabase.openOrCreateDatabase("my_database.db", null);
}
public void queryData(String sql) {
// 执行查询操作的代码
}
}
在单例模式中,static关键字起着重要作用。对于成员变量,如instance,使用static修饰后,它属于类而不是类的实例,在内存中只有一份拷贝,所有实例都共享这个变量。对于方法getInstance(),static使得该方法可以直接通过类名调用,而不需要创建类的实例。
在初始化过程中,当第一次调用getInstance()方法时,如果instance为null,则创建一个新的DatabaseManager实例,同时初始化数据库连接。
在单例模式中,构造函数通常被声明为private,这样可以防止外部通过new关键字创建多个实例,保证只有通过getInstance()方法才能获取到唯一的实例。
在纸上写了个 Activity,在类里面定义了一个 Handler,会有警告,怎么解决?(涉及 static、软引用相关知识)
在 Activity 类中定义 Handler 时,如果直接定义可能会出现内存泄漏的警告。这是因为 Handler 默认会持有外部类(即 Activity)的引用,当 Activity 销毁时,如果 Handler 中还有未处理完的消息,那么 Activity 就无法被正常回收,从而导致内存泄漏。
解决方法是使用静态内部类结合软引用的方式来定义 Handler。静态内部类不会持有外部类的隐式引用,而软引用可以在内存不足时被系统回收。
public class MainActivity extends AppCompatActivity {
private MyHandler myHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myHandler = new MyHandler(this);
// 发送消息等操作
myHandler.sendEmptyMessage(0);
}
@Override
protected void onDestroy() {
super.onDestroy();
// 移除所有未处理的消息
myHandler.removeCallbacksAndMessages(null);
}
// 静态内部类
private static class MyHandler extends Handler {
// 软引用持有Activity
private final WeakReference<MainActivity> activityWeakReference;
public MyHandler(MainActivity activity) {
activityWeakReference = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = activityWeakReference.get();
if (activity!= null) {
// 处理消息的逻辑
}
}
}
}
这样,当 Activity 销毁后,即使 Handler 中还有未处理的消息,由于软引用的存在,在内存不足时,Activity 也可以被正常回收,避免了内存泄漏。同时,在 Activity 销毁时,还需要调用removeCallbacksAndMessages(null)方法来移除 Handler 中所有未处理的消息,进一步确保资源的正确释放。
画图说明 android 消息机制中 handler、looper、message、messagequeue 的关系
很遗憾没办法直接画图,但我可以用文字详细描述它们之间的关系,你可以根据描述自行绘制。
想象一个 “消息处理流水线”,Handler、Looper、Message和MessageQueue各司其职,共同构成 Android 消息机制。
Message是消息载体,它携带了要处理的信息,例如一个 Runnable 对象或者一些简单数据。Message可以通过Handler的obtainMessage()等方法获取,也可以直接new Message()创建。
MessageQueue则像一个消息队列仓库,它采用先进先出(FIFO)的方式存储Message。Handler发送的消息都会进入这个队列等待处理。MessageQueue内部使用一个单链表的数据结构来管理消息,保证消息有序存储。
Looper是消息循环的核心,它就像一个勤劳的 “消息快递员”。Looper不断地从MessageQueue中取出消息,然后分发给对应的Handler处理。每个线程只能有一个Looper,它通过prepare()方法来初始化,并通过loop()方法启动消息循环。
Handler是消息的发送者和处理者。一方面,它可以通过sendMessage()、post()等方法将Message发送到MessageQueue中。另一方面,它需要重写handleMessage()方法来处理从Looper分发过来的消息。Handler在创建时会与特定的Looper关联,从而知道从哪个MessageQueue获取消息。
整体来看,Handler把Message发送到MessageQueue,Looper从MessageQueue取出Message交给Handler处理,它们紧密协作,确保 Android 应用中的消息能够有序、高效地处理。
说明 threadlocal 原理
ThreadLocal是 Java 提供的一种线程本地存储机制,它为每个使用该变量的线程都提供一个独立的变量副本,每个线程可以独立地修改自己的副本,而不会影响其他线程的副本。
从原理上讲,ThreadLocal内部维护了一个ThreadLocalMap,这是一个定制化的哈希表。当一个线程通过ThreadLocal的set()方法设置值时,实际上是将当前线程作为键,设置的值作为值,存储到ThreadLocalMap中。例如:
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("Hello, ThreadLocal");
在这个例子中,ThreadLocal会将当前线程与字符串 “Hello, ThreadLocal” 关联起来,存储在ThreadLocalMap中。
当线程通过get()方法获取值时,ThreadLocal会以当前线程为键,在ThreadLocalMap中查找对应的副本值。如果没有找到,可能会返回默认值(如果设置了initialValue()方法)。
String value = threadLocal.get();
这种机制使得每个线程都有自己独立的变量副本,在多线程环境下,避免了不同线程之间对共享变量的竞争和干扰,提高了线程的安全性和隔离性。
ThreadLocal的应用场景广泛,比如在数据库连接管理中,每个线程都需要一个独立的数据库连接,使用ThreadLocal可以方便地为每个线程创建并管理自己的数据库连接,而不用担心线程间的干扰。
分析 handler 内存泄露的原因及解决方法
Handler 内存泄漏通常是由于 Handler 对外部类(如 Activity)的隐式引用导致的。
在 Android 中,当在 Activity 中定义一个非静态内部类的 Handler 时,这个 Handler 会隐式持有外部 Activity 的引用。如果此时 Activity 销毁,但 Handler 中还有未处理完的消息,这些消息会在消息队列中继续存在,由于消息持有 Handler 的引用,而 Handler 又持有 Activity 的引用,从而形成了一个引用链,导致 Activity 无法被垃圾回收器回收,最终造成内存泄漏。
以下是解决 Handler 内存泄漏的常见方法:
- 使用静态内部类和弱引用:将 Handler 定义为静态内部类,静态内部类不会持有外部类的隐式引用。同时,在静态内部类中使用弱引用指向外部 Activity。这样,即使 Handler 中有未处理的消息,当 Activity 销毁时,由于弱引用的特性,在内存不足时,Activity 可以被回收。例如:
public class MainActivity extends AppCompatActivity {
private MyHandler myHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myHandler = new MyHandler(this);
myHandler.sendEmptyMessage(0);
}
@Override
protected void onDestroy() {
super.onDestroy();
myHandler.removeCallbacksAndMessages(null);
}
private static class MyHandler extends Handler {
private final WeakReference<MainActivity> activityWeakReference;
public MyHandler(MainActivity activity) {
activityWeakReference = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = activityWeakReference.get();
if (activity!= null) {
// 处理消息
}
}
}
}
- 在 Activity 销毁时移除消息:在 Activity 的
onDestroy()方法中,调用 Handler 的removeCallbacksAndMessages(null)方法,移除 Handler 中所有未处理的消息和回调,这样可以确保在 Activity 销毁时,不会因为 Handler 中的消息而导致内存泄漏。
说明 handler.removecallback... 方法为什么可能不能将消息从消息队列移除
Handler的removeCallbacksAndMessages()方法用于从消息队列中移除指定的消息或回调,但有时它可能无法将消息从消息队列移除,原因主要有以下几点:
消息已经开始处理
当消息队列中的消息被Looper取出并正在交给Handler处理时,此时调用removeCallbacksAndMessages()方法,可能无法移除该消息。因为Looper在处理消息时,会先将消息从队列中取出,然后执行相关操作,在这个过程中,Handler的移除操作无法影响已经取出的消息。
消息队列正在遍历
MessageQueue内部在某些情况下会对队列进行遍历操作,例如在Looper循环取出消息时。如果在这个遍历过程中调用removeCallbacksAndMessages()方法,由于多线程环境下可能存在的并发问题,可能无法准确地移除消息。虽然MessageQueue内部有一定的同步机制,但在复杂的情况下,可能会出现竞争条件,导致移除操作失败。
消息匹配不准确
removeCallbacksAndMessages()方法需要准确匹配要移除的消息或回调。如果传入的参数(如Runnable对象、Message对象)与队列中的消息不精确匹配,就无法成功移除。例如,如果传入的Runnable对象与队列中消息所关联的Runnable对象不是同一个实例(即使它们的逻辑相同),也不能移除消息。
消息推送如果由于网络时延造成集中通知的现象,怎么提高用户体验?
当消息推送因网络时延导致集中通知时,可从以下几个方面提高用户体验:
优化通知策略
- 合并通知:对于相同类型或相关的消息,进行合并显示。例如,多个新闻推送可以合并为一条通知,显示 “您有 X 条新闻未读”,用户点击后可以展开查看详细内容。这样可以避免大量相似通知造成的干扰。
- 智能排序:根据消息的重要性、时效性等因素对通知进行排序。重要紧急的消息优先显示,比如涉及账户安全的通知,而一些普通的资讯类消息可以排在后面。这样用户可以先关注重要信息,提高信息获取效率。
本地缓存与预加载
- 本地缓存:在设备本地缓存一定数量的最近推送消息。当网络恢复,大量消息集中到达时,先检查本地缓存,避免重复显示已经缓存过的消息,减少用户看到重复内容的概率。
- 预加载:对于一些可能会频繁推送的消息类型,提前在后台预加载部分内容。例如,对于新闻推送,可以预加载新闻的标题和简短摘要,当通知到达时,用户可以更快地获取关键信息,减少等待时间。
网络优化与反馈
- 网络优化:在应用层面,优化网络请求策略,采用更高效的网络协议,减少网络请求的次数和数据量。例如,合并多个小的网络请求为一个大请求,减少网络连接的建立和断开次数,从而降低网络时延。
- 反馈机制:向用户提供网络状态和消息推送状态的反馈。比如显示 “消息正在加载,请稍候”,让用户了解到集中通知是由于网络问题导致的,而不是应用出现故障,缓解用户的焦虑情绪。
个性化设置
- 通知设置:提供个性化的通知设置选项,允许用户根据自己的需求设置消息推送的频率、时间、类型等。例如,用户可以设置只在特定时间段接收某些类型的消息,避免在休息时间被过多通知打扰。
toast 如果短时间内频繁显示怎么优化,提升用户体验?
当Toast短时间内频繁显示时,可能会导致用户体验不佳,以下是一些优化方法:
- 合并 Toast 内容:如果短时间内要显示的
Toast内容相同,可以考虑将它们合并为一个Toast,并适当延长显示时间。例如,在一个循环中需要多次显示相同的提示信息,可以使用一个变量记录显示次数,然后在循环结束后显示一次包含次数的提示,如 “已成功操作 5 次”。 - 使用队列机制:创建一个
Toast队列,当有新的Toast需要显示时,将其加入队列。当前一个Toast显示完成后,再从队列中取出下一个Toast进行显示,这样可以避免多个Toast重叠显示的问题。 - 设置显示间隔:记录上一次
Toast显示的时间,当新的Toast要显示时,判断距离上一次显示是否超过了一定的时间间隔。如果未超过,则不显示新的Toast,或者更新当前显示的Toast内容。 - 自定义 Toast 布局:可以自定义
Toast的布局,使其更加美观和易于阅读。例如,增加背景透明度、调整文字大小和颜色等,让用户更容易看清提示内容,即使在频繁显示时也能快速获取关键信息。
手机被 root 了怎么保证数据安全?
手机被root后,数据安全面临一定风险,可采取以下措施来保证数据安全:
- 安装安全软件:选择可靠的手机安全软件,这类软件通常具有检测恶意软件、防范病毒攻击等功能,可以对手机系统和数据进行实时监控和保护。有些安全软件还提供隐私保护功能,如加密短信、通话记录等。
- 谨慎授予权限:对于需要获取敏感权限的应用要谨慎授权。仔细查看应用所需的权限列表,对于不必要的权限坚决不授予。比如,一个普通的工具类应用如果请求获取通讯录、短信等权限,就需要特别警惕,很可能存在风险。
- 数据加密:利用手机系统自带的数据加密功能或第三方加密工具,对重要数据进行加密处理。这样即使手机数据被窃取,没有正确的密钥也无法获取其中的内容。例如,可以对存储在手机中的文档、照片、视频等文件进行加密。
- 定期备份数据:将手机中的重要数据定期备份到云存储或外部存储设备上。一旦手机出现问题或数据丢失,可以及时从备份中恢复数据。备份频率可以根据数据的重要性和更新频率来确定,如每周或每月备份一次。
像微信这样的大用户量应用怎么提高查询速度,不包括索引、分表、分库这样的传统方式?
对于像微信这样的大用户量应用,除传统方式外,还可通过以下方法提高查询速度:
- 缓存技术:采用多级缓存策略,如内存缓存和本地磁盘缓存。对于经常查询的热门数据,如用户的聊天记录摘要、好友列表等,缓存在内存中,以便快速读取。同时,将一些不常变化但需要持久化的数据缓存到本地磁盘,当内存缓存失效时,可以从磁盘缓存中获取。
- 数据预处理:在数据入库前对其进行预处理,例如对文本数据进行分词处理,将长文本拆分成一个个关键词,这样在查询时可以直接根据关键词进行快速匹配,而不需要对整个文本进行扫描。
- 分布式搜索框架:引入分布式搜索框架,如 Elasticsearch 等。它可以将数据分布在多个节点上进行存储和索引,支持大规模数据的快速检索。通过对数据进行合理的分片和副本设置,提高查询的并发处理能力和数据的可靠性。
- 优化查询算法:使用更高效的查询算法和数据结构。例如,在查找好友列表时,可以使用哈希表或二叉搜索树等数据结构来提高查找速度。对于模糊查询,可以采用模糊匹配算法,如编辑距离算法等,快速找到与查询条件相似的结果。
分析 surfaceview 与 view 的区别与优点,说明 surfaceview 在什么情况下会重新绘制,而不是在原有基础下绘制?
SurfaceView和View的区别与优点如下:
- 绘制机制:
View在主线程中进行绘制,而SurfaceView有自己独立的绘制线程。这使得SurfaceView可以在不阻塞主线程的情况下进行复杂的绘制操作,适用于处理高帧率的动画、视频播放等场景。 - 显示层级:
SurfaceView在Window中是单独的一层,位于View所在层级之下。它可以实现一些特殊的效果,如半透明、覆盖在其他View之上等。 - 性能表现:由于
SurfaceView的独立线程绘制,在处理大量图像数据或频繁更新画面时,性能更优,能避免View因在主线程绘制导致的卡顿现象。 - 适用场景:
View适用于常规的界面元素展示和交互,如按钮、文本框等。SurfaceView则更适合需要实时更新画面、对性能要求高的场景,如游戏开发、视频预览等。
SurfaceView会在以下情况重新绘制:
- SurfaceHolder 的回调:当
Surface的创建、销毁或大小改变时,SurfaceHolder.Callback的相应方法会被调用,此时通常需要重新绘制。 - 数据变化:如果绘制的数据源发生了变化,如视频播放中的下一帧数据到来、游戏中的角色位置更新等,需要根据新的数据重新绘制。
- 显示区域变化:当
SurfaceView的显示区域被其他窗口遮挡后又重新显示,或者SurfaceView的大小、位置发生改变时,也可能需要重新绘制。
介绍 Retrofit 的优点好处,是否用过 MaterialDesign 的控件?
Retrofit是一款流行的 Android 网络请求框架,具有以下优点:
- 简洁易用:
Retrofit的 API 设计简洁明了,使用注解的方式来定义网络请求接口,大大简化了网络请求的代码编写。开发者只需定义好接口和请求方法,Retrofit就能自动完成网络请求的发送和响应的解析。 - 强大的扩展性:它支持多种数据格式的解析,如 JSON、XML 等,并且可以通过添加转换器来支持更多的数据格式。还能与其他框架如 RxJava 结合,实现异步操作和响应式编程,方便进行数据处理和流程控制。
- 高效的网络请求:
Retrofit底层基于 OkHttp,具有高效的网络请求性能,支持连接池、缓存策略等功能,能够提高网络请求的效率,减少网络开销。 - 易于维护和更新:由于采用了接口和注解的方式,当网络接口发生变化时,只需要在接口中进行相应的修改,而不需要在大量的代码中查找和修改网络请求的逻辑,提高了代码的可维护性和可扩展性。
关于MaterialDesign控件,它是 Google 推出的一套设计规范和控件库,具有以下特点和优势:
- 美观的设计风格:遵循
MaterialDesign规范的控件具有统一、美观的设计风格,采用了卡片式布局、涟漪效果等元素,使应用界面更加时尚、有质感。 - 良好的交互体验:提供了丰富的交互效果和动画,如过渡动画、状态切换动画等,能够为用户带来流畅、自然的交互体验。
- 方便的开发集成:Android 系统提供了对
MaterialDesign控件的支持,开发者可以很方便地在项目中引入和使用这些控件,如Toolbar、FloatingActionButton等,减少了自定义控件的开发工作量。
在实际开发中,使用MaterialDesign控件可以快速构建出符合现代设计风格的应用界面,提升应用的用户体验和视觉效果。
说明消息机制中 Handler 是怎么获取 looper 对象的,messageQueue 是怎么获取 message 的,是死循环还是轮询,涉及生产者消费者原理吗?
在 Android 消息机制中,Handler获取Looper对象是通过Looper.myLooper()方法。在Handler的构造函数中,如果没有传入指定的Looper,就会调用Looper.myLooper()来获取当前线程的Looper。如果当前线程没有Looper,则会抛出异常。
MessageQueue获取Message是通过不断地从队列中读取。它内部是通过一个for(;;)的无限循环来实现的,也就是所谓的死循环,但更准确地说它是一种轮询机制。在循环中,通过nativePollOnce()方法阻塞等待消息的到来,当有消息入队时,会被唤醒并从队列中取出消息进行处理。
这一过程涉及到生产者消费者原理。Handler发送消息相当于生产者,将Message生产出来并放入MessageQueue中。而Looper从MessageQueue中取出消息并交给Handler处理,相当于消费者。MessageQueue则作为消息的缓冲区,协调着生产者和消费者之间的关系,保证消息的有序处理。
说明布局优化方法,如 merge、include、viewstub 的使用。
- **
merge**标签:主要用于减少布局的层级。当一个布局文件作为其他布局的子布局被包含时,如果该布局的根节点是FrameLayout且没有多余的属性和背景等,就可以使用merge标签来替换。这样在被包含时,merge标签内的子视图会直接添加到包含它的布局中,避免了多余的一层FrameLayout。 - **
include**标签:可以将一个布局文件包含到另一个布局文件中,实现布局的复用。比如在多个界面中都有相同的头部或底部布局,就可以将这些布局单独写在一个文件中,然后在需要的地方使用include标签引入。可以通过设置android:id等属性来为包含的布局指定不同的标识,方便在代码中进行操作。 ViewStub:是一个轻量级的视图,它在布局加载时默认不占用任何资源,只有在通过代码调用setVisibility(View.VISIBLE)或inflate()方法时才会真正加载其指定的布局资源。适用于一些在某些特定条件下才需要显示的布局,比如网络异常时的提示布局等,这样可以提高布局的加载效率,减少不必要的资源消耗。
介绍 android 中有哪些动画,分析 view 动画与属性动画的区别。
Android 中的动画主要有以下几种类型:
- View 动画:也叫补间动画,包括平移、旋转、缩放和透明度变化四种基本类型。可以通过 XML 文件或代码来定义,它是对 View 的影像进行操作,而不是真正改变 View 的属性,动画结束后 View 会回到原来的位置和状态。
- 属性动画:可以对任何对象的属性进行动画操作,不仅仅局限于 View。它通过不断地改变对象的属性值来实现动画效果,动画结束后对象的属性会保持在最终的值。属性动画可以实现更复杂和灵活的动画效果,比如对自定义 View 的某个属性进行动画操作等。
- Drawable 动画:主要用于对 Drawable 资源进行动画处理,如帧动画,通过顺序播放一系列的 Drawable 图片来实现动画效果,常用于一些简单的动画场景,如加载动画等。
View 动画和属性动画的区别主要体现在以下几个方面:
- 操作对象:View 动画主要针对 View 进行操作;属性动画可以对任意对象的属性进行操作。
- 改变本质:View 动画只是改变 View 的显示效果,不改变 View 的实际属性;属性动画是真正改变对象的属性值。
- 兼容性:View 动画在低版本的 Android 系统上兼容性较好;属性动画在低版本上可能存在一些兼容性问题。
- 灵活性:属性动画比 View 动画更加灵活,可以实现更多复杂的动画效果,如组合动画、自定义属性动画等。
说明 android 网络优化方法。
- 缓存策略:合理使用缓存可以减少网络请求次数,提高应用的响应速度。对于一些不经常变化的数据,如图片、新闻内容等,可以在本地缓存。可以使用内存缓存和磁盘缓存相结合的方式,如使用
LruCache来管理内存缓存,DiskLruCache来管理磁盘缓存。 - 数据压缩:在发送和接收数据时,对数据进行压缩可以减少数据传输量,降低网络带宽占用。可以使用
GZIP等压缩算法对数据进行压缩,在服务器端和客户端进行相应的解压缩操作。 - 优化请求时机:避免在网络状况不佳时进行大量数据的请求,可以通过监听网络状态,当网络连接良好时再进行重要数据的请求。同时,尽量合并多个小的网络请求为一个大的请求,减少网络连接的建立和断开次数。
- 使用 HTTP/2:HTTP/2 相比 HTTP/1.1 有很多优势,如多路复用、头部压缩等,可以提高网络传输效率。在应用中尽量使用支持 HTTP/2 的网络库,如 OkHttp 等。
- 图片优化:对于图片资源,根据不同的设备分辨率和屏幕大小,加载合适尺寸的图片,避免加载过大的图片造成网络带宽浪费。可以使用图片加载库,如 Glide、Picasso 等,它们可以自动进行图片的压缩和缓存处理。
说明自定义 view 过程中的三个测量模式,怎样从 onMesure 的两个参数中获取长度?
自定义 View 过程中的三个测量模式如下:
- EXACTLY:精确模式,当布局文件中为 View 指定了具体的宽度或高度值,或者使用
match_parent时,View 会采用精确模式,此时测量出的尺寸就是指定的尺寸。 - AT_MOST:最大值模式,当布局文件中使用
wrap_content时,View 会采用最大值模式,此时 View 的尺寸不能超过父容器所允许的最大尺寸。 - UNSPECIFIED:未指定模式,这种情况比较少见,一般在系统内部使用,父容器不对 View 的尺寸做任何限制,View 可以随意设置自己的尺寸。
在onMeasure方法中,有两个参数widthMeasureSpec和heightMeasureSpec,它们包含了测量模式和尺寸信息。可以通过以下方式获取长度:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 根据不同的测量模式来计算View的宽度和高度
if (widthMode == MeasureSpec.EXACTLY) {
// 精确模式,直接使用指定的宽度
width = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
// 最大值模式,根据内容计算宽度,但不能超过widthSize
width = calculateWidthBasedOnContent();
width = Math.min(width, widthSize);
} else {
// UNSPECIFIED模式,根据需要设置宽度
width = defaultWidth;
}
// 同理处理高度
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
height = calculateHeightBasedOnContent();
height = Math.min(height, heightSize);
} else {
height = defaultHeight;
}
setMeasuredDimension(width, height);
}
讲一遍事件分发机制,然后问 View A,View B 事件传递过来,B onIntercept 返回了 true,接下来的事件是怎样的?
Android 的事件分发机制主要涉及三个方法:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。
- 事件传递顺序:首先,事件会从 Activity 传递到最外层的 ViewGroup,由 ViewGroup 的
dispatchTouchEvent方法开始处理。如果 ViewGroup 不拦截事件,即onInterceptTouchEvent返回 false,事件会继续传递给子 View 的dispatchTouchEvent方法。如果没有子 View 或者子 View 不处理事件,那么 ViewGroup 会自己处理事件,调用onTouchEvent方法。 - 事件拦截处理:当 View B 的
onInterceptTouchEvent返回 true 时,表示 View B 拦截了该事件。后续的同类型触摸事件(如 MOVE、UP 等)将不再传递给 View B 的子 View,而是直接由 View B 的onTouchEvent方法来处理。而 View A 则不会再接收到该事件系列后续的事件,除非有新的触摸事件从 Activity 重新开始分发。
介绍类加载,双亲委托,两个类全限定名一样,都会加载吗,怎么打破双亲委托,怎么自己继承写个 classloader?
- 类加载:是将类的字节码文件加载到内存,并创建出对应的 Class 对象的过程。在 Android 中,类加载器负责从不同的来源(如本地文件系统、网络等)加载类的字节码。
- 双亲委托:当一个类加载器收到类加载请求时,它首先不会自己去尝试加载这个类,而是把请求委托给父类加载器去完成,依次向上,直到顶层的启动类加载器。只有当父类加载器无法加载这个类时,子类加载器才会尝试自己去加载。
- 同名类加载:如果两个类全限定名一样,在双亲委托机制下,通常只会被加载一次。因为类加载器会先检查已加载的类中是否已有相同全限定名的类。
- 打破双亲委托:可以通过重写类加载器的
loadClass方法,改变类的加载顺序,不先委托给父类加载器,而是自己先尝试加载。 - 自定义 ClassLoader:继承
ClassLoader类,重写findClass方法。在findClass方法中实现自己的类加载逻辑,比如从指定的路径读取字节码文件,将字节码转换为 Class 对象并返回。
ListView 加载网络图片,怎样防止图片错乱?
在 ListView 加载网络图片时防止图片错乱,有以下几种方法:
- 使用 ViewHolder 模式:为 ListView 的每个 Item 创建一个 ViewHolder,用于缓存 Item 中的视图组件,避免每次都重新查找视图。在绑定数据时,根据 Item 的位置设置对应的图片,确保图片与 Item 正确对应。
- 设置 Tag 标记:在为 ImageView 设置图片时,同时设置一个与图片 URL 或其他唯一标识对应的 Tag。在图片加载完成后,检查 ImageView 的 Tag 是否与当前加载的图片标识一致,一致则设置图片,否则不设置,防止图片错位。
- 使用图片加载框架:如 Glide、Picasso 等,这些框架内部已经处理了图片加载的异步任务和防止图片错乱的问题。它们会根据 ImageView 的生命周期来管理图片加载,确保在正确的时机加载和显示图片。
介绍 dp 与 px 转换公式,为什么用 dp?
- dp 与 px 转换公式:在 Android 中,px(像素)是屏幕上的实际像素点,dp(密度无关像素)是一种基于屏幕密度的抽象单位。它们之间的转换公式为
px = dp * (dpi / 160),其中 dpi 是屏幕的密度。例如,在 hdpi 屏幕(dpi 为 240)上,1dp 等于 1.5px;在 xhdpi 屏幕(dpi 为 320)上,1dp 等于 2px。 - 使用 dp 的原因:使用 dp 可以让应用在不同屏幕密度的设备上保持相对一致的布局和视觉效果。如果使用 px 来定义布局尺寸,在高密度屏幕上,元素会显得很小,在低密度屏幕上,元素会显得很大。而使用 dp,系统会根据屏幕密度自动将 dp 转换为合适的 px 值,使得界面元素在不同设备上的大小和间距相对稳定,提高了应用的兼容性和用户体验。
介绍打包签名流程,keystore 是用来干嘛的,二次打包,包名一样,签名不一样可以同时安装吗?
- 打包签名流程:首先,将 Android 项目中的资源文件、Java 代码等进行编译,生成对应的字节码文件和资源索引文件。然后,将这些文件进行打包,生成未签名的 APK 文件。最后,使用数字证书对 APK 文件进行签名,生成最终可安装的 APK。
- keystore 的作用:keystore 是用于存储数字证书的文件,它包含了开发者的公钥、私钥和证书等信息。在打包签名过程中,使用 keystore 中的私钥对 APK 进行签名,生成数字签名。系统在安装 APK 时,会使用 keystore 中的公钥来验证签名的合法性,确保 APK 没有被篡改。
- 二次打包安装问题:如果包名一样,签名不一样,是不可以同时安装的。因为 Android 系统通过包名和签名来唯一标识一个应用。当安装一个新的 APK 时,系统会检查包名和签名,如果包名相同但签名不同,系统会认为这是一个不同的应用,会提示冲突,无法同时安装。
一键结束全部程序,activity 会不调用 onStop,onDestory,被强杀怎样恢复数据?
在 Android 中,当一键结束全部程序或者应用被系统强杀时,Activity确实可能不会调用onStop和onDestroy方法。对于恢复数据,可以采用以下几种常见方式:
- 使用**
onSaveInstanceState和onRestoreInstanceState**:在Activity可能被销毁之前,系统会调用onSaveInstanceState方法,开发者可以在此方法中保存关键数据到Bundle中,比如当前界面的一些状态、用户输入的文本等。当Activity重新创建时,onRestoreInstanceState方法会被调用,可从Bundle中取出数据恢复界面状态。 - 持久化存储:将数据保存到本地文件或者数据库中。比如使用
SharedPreferences存储简单的键值对数据,像用户的配置信息等;对于复杂数据,可以使用SQLite数据库进行存储。当应用重新启动后,从这些存储介质中读取数据进行恢复。 - 网络数据同步:如果数据是从网络获取的,并且服务器端有数据备份,那么在应用重启后,可以重新从服务器获取数据来恢复界面显示。同时,也可以在数据发生变化时,及时将数据同步到服务器,以保证数据的完整性和一致性。
说明 ANR 几秒?
在 Android 系统中,通常会出现以下几种 ANR(Application Not Responding)情况及对应的时间阈值:
- 输入事件超时:如果应用在 5 秒内没有响应输入事件,如按键按下、屏幕触摸等,就可能会出现 ANR。这是因为用户的输入操作需要应用及时处理并给出反馈,如果长时间没有响应,会严重影响用户体验,所以系统设定了 5 秒的超时时间。
- BroadcastReceiver 超时
- 前台广播:对于前台
BroadcastReceiver,如果在 10 秒内没有执行完onReceive方法,就会发生 ANR。前台广播通常是比较重要且需要及时处理的广播,比如电话呼入等广播,所以给了相对较短的处理时间。 - 后台广播:后台
BroadcastReceiver的超时时间是 60 秒。因为后台广播一般不是非常紧急,对实时性要求相对较低,所以系统给予了更长的时间来处理。
- 前台广播:对于前台
- Service 超时
- 启动服务:如果在 20 秒内没有完成服务的启动操作,会出现 ANR。
- 绑定服务:对于绑定服务,如果在 1 分钟内没有完成绑定操作,也会触发 ANR。
介绍 Java 八种基本类型,int 几个字节,范围,和 Integer 的区别。
Java 中有八种基本数据类型,分别是:
- 整数类型:
byte、short、int、long。 - 浮点数类型:
float、double。 - 字符类型:
char。 - 布尔类型:
boolean。
其中,int类型占 4 个字节。它的取值范围是 - 2,147,483,648 到 2,147,483,647。
int和Integer的区别主要体现在以下几个方面:
- 数据类型:
int是基本数据类型,而Integer是int的包装类,属于引用数据类型。 - 默认值:
int的默认值是 0,而Integer的默认值是null。 - 内存分配:
int变量直接存储数值,在栈内存中分配空间。Integer对象需要在堆内存中分配空间,通过引用指向具体的对象。 - 功能方法:
Integer类提供了许多实用的方法,如parseInt用于将字符串解析为整数,valueOf用于获取Integer对象等,而int类型本身没有这些方法。
说明 Android 有几种进程,它们的优先级。
Android 系统中主要有以下几种进程,按照优先级从高到低排列如下:
- 前台进程:这是对用户来说最重要的进程,通常是用户正在直接交互的应用程序。比如正在显示在屏幕上的
Activity,或者正在执行前台Service的进程等。前台进程会在系统资源紧张时最后被杀死。 - 可见进程:该进程中的
Activity没有在前台显示,但对用户仍然可见,比如弹出的对话框等。可见进程的优先级仅次于前台进程,当系统资源不足以维持所有前台进程运行时,才会考虑杀死可见进程。 - 服务进程:用于执行后台服务的进程,如音乐播放服务、文件下载服务等。服务进程没有用户界面,但在后台执行一些重要的任务。它的优先级低于可见进程,但高于后台进程和空进程。
- 后台进程:包含的
Activity已经不可见,即已经执行了onStop方法。这些进程对用户体验没有直接影响,系统会在内存不足时优先杀死后台进程来释放资源。 - 空进程:不包含任何活动组件的进程,仅作为缓存,以缩短下次应用启动的时间。空进程的优先级最低,系统会在内存紧张时首先清理空进程。
分析 view 事件分发的各种情况,view 的绘制过程。
View 事件分发的各种情况
- 正常情况:当一个触摸事件发生时,首先会传递到
Activity的dispatchTouchEvent方法,然后由Activity传递给最外层的ViewGroup。ViewGroup会先调用dispatchTouchEvent方法,接着在该方法中调用onInterceptTouchEvent方法来判断是否要拦截事件。如果不拦截,事件会继续传递给子View;如果拦截,那么后续的事件就会在当前ViewGroup中处理,由它的onTouchEvent方法来处理。 - 特殊情况
- 子 View 消耗事件:如果子
View的onTouchEvent方法返回true,表示它消耗了该事件,那么后续的事件就不会再传递给其他View,而是继续由该子View的onTouchEvent方法处理。 - 父 View 强制拦截:父
ViewGroup可以在onInterceptTouchEvent方法中根据一定条件始终返回true,强制拦截所有事件,这样子View就无法接收到事件。 - 多层嵌套:在多层
ViewGroup嵌套的情况下,事件会从最外层的ViewGroup开始依次向下传递,每一层都可以选择拦截或者继续传递,直到找到最终处理事件的View。
- 子 View 消耗事件:如果子
View 的绘制过程
- 测量(Measure):确定
View的宽和高。在这个阶段,View会调用onMeasure方法,通过测量模式和父容器的约束来计算自己的宽高。 - 布局(Layout):确定
View在父容器中的位置。View会调用onLayout方法,根据测量得到的宽高和父容器的布局规则,确定自己在父容器中的具体位置。 - 绘制(Draw):将
View绘制到屏幕上。View会调用onDraw方法,在这个方法中进行具体的图形绘制操作,比如绘制文本、图形、图片等。