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

水滴Android面经及参考答案

static 关键字有什么作用,它修饰的方法可以使用非静态的成员变量吗?

static关键字在 Java 中有多种作用。首先,它可以用来修饰变量,被static修饰的变量称为静态变量。静态变量属于类,而不属于类的某个具体实例,它在类加载时被初始化,并且在内存中只有一份,所有该类的实例都共享这个静态变量。例如,在一个统计网站访问量的程序中,可以使用静态变量来记录访问次数,无论创建多少个相关类的实例,访问量都是统一记录和更新的。

其次,static可以修饰方法,即静态方法。静态方法同样属于类,可以直接通过类名来调用,而不需要创建类的实例。静态方法通常用于执行一些与类相关的通用操作,比如数学计算工具类中的方法,像Math.sqrt()就是静态方法,用于计算平方根,不需要创建Math类的实例就可以使用。

再者,static还可以修饰代码块,称为静态代码块。静态代码块在类加载时执行,且只执行一次,常用于对静态变量进行初始化等操作。

static修饰的方法不能直接使用非静态的成员变量。因为静态方法是属于类的,在类加载时就已经存在,而此时可能还没有创建任何类的实例,非静态成员变量是属于具体实例的,只有在创建实例后才会分配内存空间。如果静态方法可以访问非静态成员变量,就可能会出现访问到不存在的变量的情况。例如,有一个类Student,其中有静态方法printName和非静态变量name,在printName方法中试图直接访问name是错误的,因为可能还没有创建Student类的实例来初始化name变量。

Java 中创建线程有几种方式?

在 Java 中,创建线程主要有以下几种方式:

  • 继承Thread类:通过继承Thread类,并重写run方法来定义线程的执行逻辑。在run方法中编写线程要执行的代码。例如:

class MyThread extends Thread {@Overridepublic void run() {System.out.println("线程正在执行");}
}
public class Main {public static void main(String[] args) {MyThread myThread = new MyThread();myThread.start();}
}

这种方式的优点是代码简单直观,缺点是由于 Java 单继承的限制,继承了Thread类后就不能再继承其他类了。

  • 实现Runnable接口:实现Runnable接口,然后实现run方法。将实现了Runnable接口的类作为参数传递给Thread类的构造函数来创建线程。例如:

class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("线程正在执行");}
}
public class Main {public static void main(String[] args) {MyRunnable myRunnable = new MyRunnable();Thread thread = new Thread(myRunnable);thread.start();}
}

这种方式的优点是可以避免单继承的限制,一个类可以同时实现多个接口,还可以方便地实现资源共享。比如多个线程可以访问同一个实现了Runnable接口的对象中的共享数据。

  • 使用CallableFuture接口:Callable接口类似于Runnable接口,但是Callablecall方法可以有返回值,并且可以抛出异常。通过FutureTask来包装Callable对象,然后将FutureTask作为参数传递给Thread类来创建线程。例如:

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;class MyCallable implements Callable<Integer> {@Overridepublic Integer call() throws Exception {System.out.println("线程正在执行");return 100;}
}
public class Main {public static void main(String[] args) {MyCallable myCallable = new MyCallable();FutureTask<Integer> futureTask = new FutureTask<>(myCallable);Thread thread = new Thread(futureTask);thread.start();try {Integer result = futureTask.get();System.out.println("线程返回结果:" + result);} catch (Exception e) {e.printStackTrace();}}
}

这种方式可以在获取线程执行结果时进行阻塞等待,方便获取线程执行的返回值。

wait 和 sleep 的区别,如何打断 sleep?

waitsleep是 Java 中用于线程暂停执行的两个方法,但它们有一些重要的区别:

  • 所属类不同:wait方法是Object类的方法,而sleep方法是Thread类的静态方法。
  • 作用范围不同:wait方法用于线程间的通信,通常在 synchronized代码块或方法中使用,用于让当前线程等待,直到其他线程调用notifynotifyAll方法来唤醒它。而sleep方法用于让当前线程暂停执行一段时间,不涉及线程间的通信,只是单纯地让线程休眠。
  • 释放锁的情况不同:当线程调用wait方法时,它会释放当前持有的对象锁,这样其他线程就可以获取该锁并访问被同步的代码块或方法。而sleep方法不会释放锁,线程在休眠期间仍然持有锁,其他线程无法访问被该线程锁住的资源。

要打断sleep可以使用interrupt方法。当一个线程在执行sleep方法时,如果另一个线程调用了该线程的interrupt方法,那么sleep方法会抛出InterruptedException异常,从而打断sleep状态。例如:

public class Main {public static void main(String[] args) {Thread thread = new Thread(() -> {try {System.out.println("线程开始休眠");Thread.sleep(5000);System.out.println("线程休眠结束");} catch (InterruptedException e) {System.out.println("线程休眠被打断");}});thread.start();try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}thread.interrupt();}
}

在上述代码中,主线程启动了一个子线程,子线程休眠 5 秒,主线程休眠 2 秒后调用子线程的interrupt方法,子线程的sleep方法被打断,捕获到InterruptedException异常并进行相应处理。

Java 垃圾回收的目的是什么,垃圾回收机制是怎样的?

Java 垃圾回收的目的主要是为了自动管理内存,避免内存泄漏和内存溢出等问题,提高程序的稳定性和性能。在 Java 程序运行过程中,会不断地创建对象来占用内存空间,当这些对象不再被程序使用时,如果不及时回收它们占用的内存,就会导致内存资源的浪费,甚至可能耗尽系统的内存资源,使程序崩溃。垃圾回收机制就是负责在适当的时候自动回收这些不再被使用的对象所占用的内存,让这些内存可以被重新分配和使用。

Java 的垃圾回收机制是基于可达性分析算法来判断对象是否可被回收。它从一系列被称为 “GC Roots” 的对象开始,通过引用关系向下搜索,如果一个对象到 “GC Roots” 没有任何引用链相连,那么这个对象就被认为是不可达的,也就是可以被回收的垃圾对象。“GC Roots” 通常包括以下几种对象:虚拟机栈中局部变量表中的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中 JNI 引用的对象等。

当垃圾回收器发现了可回收的对象后,会使用不同的垃圾回收算法来回收这些对象占用的内存。常见的垃圾回收算法有标记 - 清除算法、复制算法、标记 - 压缩算法、分代收集算法等。

  • 标记 - 清除算法:首先标记出所有需要回收的对象,然后在标记完成后统一回收所有被标记的对象。这种算法的缺点是会产生大量不连续的内存碎片,导致后续分配大对象时可能无法找到足够的连续内存。
  • 复制算法:将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块内存用完了,就将还存活的对象复制到另一块上面,然后把已使用过的内存空间一次性清理掉。这种算法适用于对象存活率较低的情况,比如新生代,它避免了内存碎片的问题,但会浪费一半的内存空间。
  • 标记 - 压缩算法:标记出所有需要回收的对象后,将存活的对象向一端移动,然后直接清理掉边界以外的内存。它解决了标记 - 清除算法产生内存碎片的问题,也不像复制算法那样浪费一半内存,但移动对象会带来一定的性能开销。
  • 分代收集算法:根据对象存活周期的不同将内存划分为不同的代,一般分为新生代和老年代。新生代中对象的存活率较低,采用复制算法进行垃圾回收;老年代中对象存活率较高,采用标记 - 压缩或标记 - 清除算法进行垃圾回收。这种算法综合了不同算法的优点,提高了垃圾回收的效率。

Java 的垃圾回收(GC)机制是如何工作的?

Java 的垃圾回收(GC)机制主要通过以下几个步骤来工作:

  • 标记阶段:垃圾回收器首先从 “GC Roots” 开始,通过引用关系遍历整个对象图,标记出所有可达的对象,这些对象是存活的,不能被回收。而那些没有被标记的对象则是不可达的,被认为是可以回收的垃圾对象。例如,在一个 Java 程序中,有一个全局变量引用了一个对象,那么这个对象就是可达的,而那些没有任何引用指向的局部对象,在方法执行完毕后就会变成不可达对象,在标记阶段就会被标记为可回收对象。
  • 清除或回收阶段:根据不同的垃圾回收算法,对标记为可回收的对象进行处理。如果使用标记 - 清除算法,那么在标记完成后,直接回收所有被标记的对象占用的内存空间。如果是复制算法,会将存活的对象复制到另一块空闲内存区域,然后清理掉原来的内存空间。对于标记 - 压缩算法,会将存活的对象向一端移动,然后清理掉边界以外的内存空间。在分代收集算法中,新生代通常使用复制算法,老年代则根据具体情况选择标记 - 压缩或标记 - 清除算法。
  • 内存整理:在回收垃圾对象后,可能会导致内存空间不连续,产生内存碎片。为了提高内存的利用率,垃圾回收器可能会进行内存整理,将存活的对象移动到连续的内存空间中,使内存空间更加紧凑。例如,在使用标记 - 压缩算法时,内存整理是在标记和清除阶段之间进行的,通过将存活对象移动到一端,来消除内存碎片。

Java 的垃圾回收机制是自动运行的,不需要程序员手动去管理内存的分配和释放。它会在程序运行过程中,根据内存的使用情况和一定的触发条件,自动启动垃圾回收操作,以保证系统有足够的内存空间来运行程序,同时提高内存的使用效率,减少内存泄漏和内存溢出等问题的发生。不过,程序员可以通过一些方法来影响垃圾回收的行为,比如调用System.gc()方法来建议垃圾回收器执行垃圾回收,但这只是一个建议,垃圾回收器不一定会立即执行。

请解释 Java 内存模型(JMM),并谈谈 volatile 关键字。

Java 内存模型(JMM)是一种抽象的概念,它定义了 Java 程序中各个线程如何访问和操作内存中的数据。JMM 规定了主内存和工作内存的概念,主内存是所有线程共享的,而每个线程都有自己的工作内存,线程对变量的操作都在工作内存中进行,然后再与主内存进行数据同步。

JMM 还规定了一些规则来保证内存可见性、原子性和有序性。例如,当一个线程修改了共享变量的值,其他线程能够及时看到这个修改,这就是内存可见性。通过一些机制,如 volatile 关键字、synchronized 关键字等,可以实现这些特性。

volatile 关键字是 Java 中的一个重要关键字。它主要有两个作用,一是保证变量的内存可见性,二是禁止指令重排序。

当一个变量被 volatile 修饰后,任何线程对该变量的修改都会立即刷新到主内存中,同时其他线程在使用该变量时,会直接从主内存中读取最新的值,而不是使用自己工作内存中的缓存值。这样就确保了不同线程之间对该变量的操作具有可见性。

例如,在多线程环境下,如果一个线程修改了一个共享的 volatile 变量,其他线程能够马上感知到这个变化。

另外,volatile 关键字还能禁止指令重排序。在 Java 程序中,为了提高性能,编译器和处理器可能会对指令进行重排序。但对于 volatile 变量,会限制这种重排序,保证其操作的顺序符合程序的语义。这在一些需要保证操作顺序的场景中非常重要,比如双重检查锁定实现单例模式时,使用 volatile 可以防止出现初始化不完全的情况。

ArrayList 与 LinkedList 的主要区别是什么?

ArrayList 和 LinkedList 是 Java 中常用的两种集合类,它们有以下主要区别。

首先是数据结构方面。ArrayList 是基于数组实现的,它内部维护了一个动态大小的数组。当元素数量超过数组容量时,会自动进行扩容。而 LinkedList 是基于链表实现的,每个元素都包含指向前一个元素和后一个元素的引用,形成一个双向链表。

在随机访问性能上,ArrayList 具有优势。由于它是基于数组的,所以可以通过索引直接访问元素,时间复杂度为 O (1)。例如,要获取 ArrayList 中第 n 个元素,可以直接通过数组的下标访问。而 LinkedList 要访问指定位置的元素,需要从链表的头部或尾部开始遍历,时间复杂度为 O (n)。

在插入和删除操作上,LinkedList 表现更好。在 LinkedList 中,插入和删除元素只需要修改相关节点的引用即可,时间复杂度为 O (1)。比如在链表中间插入一个元素,只需要将新元素的前后引用指向正确的节点,并更新相邻节点的引用。而 ArrayList 在插入和删除元素时,可能需要移动大量的元素来保持数组的连续性,时间复杂度为 O (n)。

内存占用也有所不同。ArrayList 需要连续的内存空间来存储元素,当数组扩容时,可能会分配比实际需求更多的内存。LinkedList 每个节点除了存储元素本身外,还需要额外的空间存储前后节点的引用,所以内存占用相对较高。

HashMap 与 HashSet 的主要区别是什么?

HashMap 和 HashSet 是 Java 中两个常用的集合类,它们之间存在一些显著的区别。

从数据结构角度来看,HashMap 是基于哈希表实现的键值对集合,它存储的是键值对(key - value)形式的数据。而 HashSet 是基于哈希表实现的不包含重复元素的集合,它内部实际上是使用 HashMap 来实现的,只不过 HashSet 只关注元素本身,将元素作为 HashMap 的键,值则使用一个固定的对象。

在用途上,HashMap 主要用于通过键来快速查找和访问对应的值。例如,在一个存储用户信息的系统中,可以使用用户名作为键,用户的详细信息作为值,通过用户名快速获取用户的其他信息。HashSet 则主要用于存储不重复的元素,常用于判断元素是否存在于集合中,或者对一组数据进行去重操作。

在操作方法上,HashMap 有 put 方法用于添加键值对,get 方法用于获取指定键对应的值等。而 HashSet 有 add 方法用于添加元素,contains 方法用于判断元素是否存在等。

另外,HashMap 允许键和值都为 null,不过键只能有一个 null。而 HashSet 只允许有一个 null 元素。

在遍历方式上,HashMap 可以通过遍历键集、值集或键值对集来进行遍历。例如,可以使用 keySet 方法获取键的集合,然后遍历键来获取对应的值。HashSet 则可以直接使用迭代器或增强 for 循环来遍历其中的元素。

讲一讲泛型,包括其原理、类型擦除,以及如何获取类型?

泛型是 Java 5.0 引入的一个重要特性,它提供了一种参数化类型的机制,使得代码可以在编译时进行更强的类型检查,提高代码的安全性和可维护性。

泛型的原理是通过在类、接口或方法中使用类型参数,来表示一种通用的类型。这样可以编写更通用的代码,适用于多种不同类型的数据,而不需要为每种具体类型都编写重复的代码。例如,定义一个泛型类 GenericClass<T>,其中 T 就是类型参数,可以在类中使用 T 来表示各种不同的类型。

类型擦除是泛型在 Java 中的一个重要概念。在编译阶段,编译器会将泛型类型信息擦除,将所有的泛型类型替换为它们的原始类型。例如,List<String> 在编译后会变成 List,这是为了保持与 Java 旧版本的兼容性。虽然在运行时无法获取到泛型的具体类型信息,但在编译时,编译器会根据泛型的定义进行类型检查,确保代码的类型安全性。

在 Java 中,可以通过一些方式来获取泛型的类型。一种常见的方法是使用反射。通过反射可以获取到类的泛型参数类型。例如,对于一个泛型类 GenericClass<T>,可以通过反射获取到它的类型参数 T 的信息。另外,在 Java 8 及以后的版本中,可以使用 ParameterizedType 接口来获取泛型类型的信息。通过获取到的 ParameterizedType 对象,可以进一步获取到实际的类型参数。例如,可以获取到 List<String> 中的 String 类型。不过需要注意的是,由于类型擦除的存在,获取泛型类型信息在某些情况下可能会受到限制,特别是在一些复杂的泛型嵌套场景中。

乐观锁和悲观锁的区别,讲两种乐观锁的实现方法。

乐观锁和悲观锁是两种不同的锁机制,用于解决多线程环境下的数据并发访问问题。

乐观锁假设在大多数情况下,数据在并发访问时不会发生冲突,只有在更新数据时才会检查是否有冲突。它采用一种比较宽松的策略,在读取数据时不会对数据进行加锁,而是在更新数据时,通过检查数据在读取后是否被其他线程修改过来判断是否可以进行更新。如果数据没有被修改过,则可以成功更新;如果数据已经被修改过,则更新失败,需要采取相应的处理措施,如重试等。

悲观锁则相反,它假设在数据访问时,一定会发生并发冲突,所以在每次访问数据时都会对数据进行加锁,以防止其他线程同时访问。只有当持有锁的线程释放锁后,其他线程才能获取锁并访问数据。这种方式可以保证数据的一致性,但在高并发场景下,可能会导致性能下降,因为大量的线程需要等待锁的释放。

以下是两种常见的乐观锁实现方法。

一种是版本号机制。在数据库表中添加一个版本号字段,每当数据被更新时,版本号就会递增。当一个线程读取数据时,会同时读取数据的版本号。在更新数据时,会将当前读取到的版本号与数据库中存储的版本号进行比较,如果两者相等,说明数据在读取后没有被其他线程修改过,可以进行更新操作,并将版本号加 1;如果版本号不相等,说明数据已经被其他线程修改过,更新失败。

另一种是 CAS(Compare and Swap)算法。CAS 是一种硬件级别的原子操作,它包含三个操作数:内存位置、预期值和新值。CAS 操作会将内存位置的值与预期值进行比较,如果相等,则将内存位置的值更新为新值;如果不相等,则不进行任何操作。在 Java 中,java.util.concurrent.atomic 包中的原子类就是基于 CAS 实现的。例如,AtomicInteger 类的 incrementAndGet 方法就是通过 CAS 来实现原子性的自增操作,它可以在多线程环境下安全地对整数进行自增,而不需要使用锁。

程序计数器是什么?它在并发切换中扮演什么角色?

程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

在并发切换中,程序计数器起着关键作用。当多个线程并发执行时,每个线程都有自己独立的程序计数器。这是因为不同线程可能处于不同的执行状态,执行着不同的代码路径。当发生线程切换时,系统会保存当前线程的程序计数器的值,以便在该线程再次被调度执行时,能够从上次暂停的位置继续执行。也就是说,程序计数器记录了线程执行的位置信息,保证了线程在并发切换后能够准确地恢复执行,不会出现混乱或错误,从而确保了多线程环境下程序执行的正确性和连续性。

Android intent 如何传递数据?

Android 中 Intent 用于在组件之间传递消息,可通过多种方式传递数据。

一种方式是使用putExtra方法。可以传递各种基本数据类型及其数组,比如putExtra(String name, int value)用于传递整数,putExtra(String name, String value)用于传递字符串等。也能传递实现了Serializable接口的对象,例如:

Intent intent = new Intent(this, SecondActivity.class);
Person person = new Person("张三", 20);
intent.putExtra("person", person);
startActivity(intent);

这里Person类实现了Serializable接口。

还可以传递实现了Parcelable接口的对象,这种方式性能更好。以传递Parcelable对象为例:

Intent intent = new Intent(this, ThirdActivity.class);
Book book = new Book("Android开发艺术探索", "任玉刚");
intent.putExtra("book", book);
startActivity(intent);

其中Book类实现了Parcelable接口。

另外,还能传递Bundle对象,Bundle可以包含多个键值对数据。先将数据放入Bundle,再把Bundle放入Intent,如:

Bundle bundle = new Bundle();
bundle.putString("key1", "value1");
bundle.putInt("key2", 100);
Intent intent = new Intent(this, FourthActivity.class);
intent.putExtras(bundle);
startActivity(intent);

Android 的事件分发机制是怎样的?

Android 的事件分发机制是一个复杂但有序的过程。

当用户触摸屏幕时,首先由ActivitydispatchTouchEvent方法开始接收触摸事件。然后,事件会传递给Window,再由Window传递给最顶层的View。接着,事件会从顶层View开始,按照View树的层次结构向下传递,即从父容器到子视图,这个过程主要通过ViewGroupdispatchTouchEvent方法来实现。

ViewGroupdispatchTouchEvent方法中,会先判断自身是否拦截事件。如果拦截,那么事件就不会继续传递给子视图,而是由该ViewGroup自己处理,调用自身的onTouchEvent方法。如果不拦截,事件会继续传递给子视图,子视图再重复上述过程,判断自己是否拦截事件。

当事件传递到最底层的View时,如果该ViewonTouchEvent方法返回true,表示它处理了该事件,事件就不会再向上传递;如果返回false,则事件会向上传递给父视图的onTouchEvent方法处理,直到有视图处理该事件或者传递到最顶层的Activity

Android 中事件分发的过程是怎样的?onClick、onTouchEvent 以及 onTouch 这三者之间的调用顺序是什么?

Android 中事件分发过程是从Activity开始,经过Window,再到ViewViewGroup。首先,Activity接收到触摸事件,通过dispatchTouchEvent方法将事件分发给WindowWindow再将事件传递给顶级View。顶级View会根据事件类型和自身状态决定是否拦截事件。如果不拦截,事件会沿着View树向下传递给子视图,子视图继续判断是否拦截。当事件到达最底层的View时,View会尝试处理事件。

对于onClickonTouchEventonTouch这三者的调用顺序,当一个触摸事件发生时,如果View设置了OnTouchListener,那么onTouch方法会先被调用。如果onTouch方法返回false,则会调用onTouchEvent方法来处理事件。在onTouchEvent方法中,如果满足点击事件的条件(如按下和抬起在同一位置等),并且View设置了OnClickListener,那么onClick方法会被调用。

例如,一个按钮被点击,首先会调用设置给按钮的OnTouchListeneronTouch方法,如果onTouch返回false,接着按钮的onTouchEvent方法会被调用,在onTouchEvent中经过一系列判断,如果确定是点击事件,那么设置给按钮的OnClickListeneronClick方法就会被调用。

Android 布局类型有哪些(例如线性布局、相对布局、帧布局、约束布局等)?

Android 有多种布局类型,各有特点和适用场景。

线性布局(LinearLayout),它是一种简单的布局方式,子视图会按照水平或垂直方向排列。通过orientation属性可以设置排列方向,如horizontal表示水平排列,vertical表示垂直排列。在线性布局中,可以通过layout_weight属性来分配子视图的权重,从而实现按比例分配空间。

相对布局(RelativeLayout),子视图通过相对位置来确定其在布局中的位置。可以通过layout_abovelayout_belowlayout_toLeftOflayout_toRightOf等属性来指定子视图相对于其他视图的位置,也可以通过layout_alignParentLeftlayout_alignParentTop等属性来指定子视图与父容器的对齐方式。相对布局可以灵活地实现各种复杂的布局效果,适合布局中元素位置相对固定的情况。

帧布局(FrameLayout),所有子视图都堆叠在布局的左上角。后添加的视图会覆盖在前面的视图之上。这种布局适用于需要将多个视图重叠显示的场景,比如在一个图片上添加一些装饰性的图标或文字。

约束布局(ConstraintLayout),是一种功能强大的布局方式。它通过约束条件来确定子视图的位置和大小。可以在设计时通过可视化工具方便地设置各种约束关系,如水平居中、垂直居中、与其他视图的间距等。约束布局可以有效地减少布局的嵌套层次,提高布局的性能,适合复杂的页面布局。

还有表格布局(TableLayout),它以表格形式排列子视图,由行和列组成。可以通过TableRow来定义行,在TableRow中添加子视图来填充列。表格布局适用于展示具有表格结构的数据。

此外,还有绝对布局(AbsoluteLayout),子视图通过绝对坐标来确定位置,但这种布局方式不推荐使用,因为它在不同屏幕尺寸和分辨率下可能会出现布局错乱的问题。

如何在一个 Activity 里面获取一个 View 的宽高?应该在哪个回调方法中获取?

在 Android 中,获取一个 View 的宽高有多种方法,且需要在合适的回调方法中进行,以确保 View 已经完成测量和布局,从而得到准确的宽高值。

一种常见的方法是使用ViewTreeObserver。可以通过view.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {})来添加一个全局布局监听器。在这个监听器的回调方法中,当 View 的布局发生变化时,就可以通过view.getWidth()view.getHeight()获取到 View 的宽高。这种方法的优点是比较灵活,能在 View 布局变化的任何时候获取到最新的宽高值。不过,需要注意在不需要监听时,要及时通过view.getViewTreeObserver().removeOnGlobalLayoutListener(this)移除监听器,以免造成内存泄漏。

另一种方法是在onWindowFocusChanged方法中获取。当 Activity 的窗口获得或失去焦点时,这个方法会被调用,此时 View 的宽高已经确定,可以直接通过view.getWidth()view.getHeight()获取。但要注意,这个方法可能会被多次调用,比如当用户切换到其他应用再切回来时,所以在使用时要根据具体情况进行处理。

还可以在onResume方法之后,通过view.post(new Runnable() {})的方式来获取。在这个Runnable中,使用view.getWidth()view.getHeight()获取宽高。这是因为post方法会将任务添加到消息队列的尾部,等 View 的测量和布局完成后才会执行,从而能获取到正确的宽高值。

A 不执行 onStop 可能是什么情况?

通常情况下,当一个 Activity 不再处于前台可见状态时,系统会调用其onStop方法。但存在一些特殊情况会导致onStop方法不被执行。

一种情况是 Activity 被异常终止。例如,当系统内存不足,而当前 Activity 又处于后台,系统可能会直接杀死该 Activity 所在的进程,以释放内存,这种情况下,onStop方法就不会被执行。另外,如果应用程序发生严重的错误,导致进程崩溃,也不会执行onStop方法。

还有一种情况是在 Activity 的启动模式设置为singleInstance时,如果新的 Activity 实例在同一个任务栈中已经存在,那么再次启动它时,系统会直接将已存在的实例带到前台,而不会执行新实例的onStop方法。因为这种启动模式下,Activity 在整个系统中只有一个实例,且独占一个任务栈,系统会尽量避免不必要的生命周期方法调用,以提高性能。

此外,当使用startActivityForResult启动一个 Activity,并且在被启动的 Activity 还没有返回结果之前,就调用了finish方法关闭当前 Activity,那么当前 Activity 的onStop方法可能不会被执行。这是因为系统在这种情况下,会优先处理startActivityForResult相关的逻辑,而可能跳过onStop方法的调用。

Android 性能优化相关

Android 性能优化涵盖多个方面,包括内存优化、布局优化、电量优化、网络优化等。

内存优化是关键的一环。要避免内存泄漏,这可能是由于持有了不必要的对象引用,导致对象无法被垃圾回收。比如,在 Activity 中注册了一个全局的监听器,但在 Activity 销毁时没有反注册,就会导致 Activity 被泄漏。还需要注意合理使用内存缓存,例如使用LruCache来缓存图片等资源,避免重复加载,减少内存占用。同时,要注意优化图片的加载,对图片进行适当的压缩和尺寸调整,以降低内存消耗。

布局优化也很重要。减少布局的层级嵌套,可以提高布局的渲染速度。可以使用include标签来复用布局,使用merge标签来减少多余的布局层级。另外,合理使用ViewStub,它是一个轻量级的 View,在需要时才会加载布局,能有效减少初始布局的加载时间。

电量优化方面,要避免频繁的网络请求和不必要的后台任务,尽量将网络请求合并,并且在合适的时机进行。对传感器的使用也要谨慎,只在必要时开启,并且及时关闭,以减少电量消耗。

网络优化则要注意缓存网络数据,避免重复请求相同的内容。同时,优化网络请求的策略,例如根据网络状态选择合适的请求方式,在网络不好时适当降低请求频率,以提高用户体验。

Android 内存泄漏的原因,如何进行定位,步骤是什么?

Android 内存泄漏是指应用程序中某些对象已经不再被使用,但由于存在强引用,导致垃圾回收器无法回收它们所占用的内存,从而造成内存的浪费。

内存泄漏的原因有很多。常见的一种是静态变量引起的内存泄漏。当静态变量持有了 Activity 或其他 Context 的引用,而这个 Activity 被销毁时,由于静态变量的生命周期贯穿整个应用程序的生命周期,导致 Activity 无法被回收,从而产生内存泄漏。还有资源未正确释放,比如文件流、数据库连接、Bitmap 等资源,如果在使用后没有正确关闭或释放,也会导致内存泄漏。另外,内部持有外部引用也容易引发内存泄漏,例如一些回调函数中,如果外部对象被销毁了,但回调函数内部还持有对它的引用,就会导致外部对象无法被回收。

定位内存泄漏可以使用一些工具,如 Android Profiler。首先,在应用程序运行过程中,通过 Android Profiler 观察内存的使用情况,查看内存是否持续增长且没有明显的回落。如果发现内存有异常增长,可以在特定的操作前后进行内存快照。比如,在执行了某个可能导致内存泄漏的功能后,立即拍摄内存快照。然后,对比不同快照之间的差异,查看哪些对象的数量在不断增加,以及这些对象的引用关系。通过分析引用链,可以找到可能导致内存泄漏的源头,确定是哪个对象持有了不应该持有的引用。另外,也可以使用 LeakCanary 等专门的内存泄漏检测工具,它能在应用程序发生内存泄漏时,自动检测并给出相关的提示信息,帮助开发者快速定位问题。

如何处理内存泄漏?用什么工具?

处理内存泄漏首先要根据定位到的原因进行针对性的修复。

如果是因为静态变量持有了不必要的引用,那么在不需要使用该引用时,将其置为null,以切断引用关系,让垃圾回收器能够回收相关对象。对于资源未正确释放的情况,要确保在使用完资源后,及时调用相应的关闭或释放方法,如关闭文件流、释放 Bitmap 等。如果是内部持有外部引用导致的内存泄漏,在外部对象销毁时,要在内部将对外部对象的引用置为null,或者使用弱引用等方式来持有外部对象,避免强引用导致的内存泄漏。

在处理内存泄漏时,可以使用一些工具来辅助。除了前面提到的 Android Profiler 和 LeakCanary 外,MAT(Memory Analyzer Tool)也是一个强大的工具。它可以对内存快照进行深入分析,帮助开发者更直观地查看内存中的对象分布、引用关系等信息。通过 MAT,可以快速定位到占用大量内存的对象,以及可能存在的内存泄漏点。另外,DDMS(Dalvik Debug Monitor Service)也能提供一些关于内存使用的基本信息,如堆内存的使用情况、对象的数量等,开发者可以通过它初步了解应用程序的内存状况,为进一步的分析提供依据。

此外,在开发过程中,养成良好的编程习惯也有助于减少内存泄漏的发生。比如,合理管理对象的生命周期,及时释放不再使用的资源,避免过度使用静态变量等。同时,要对应用程序的内存使用有清晰的认识,定期进行性能测试和内存分析,及时发现和解决潜在的内存泄漏问题。

如何进行内存优化?

内存优化是提升 Android 应用性能的关键环节。首先,要注意对象的合理使用与及时回收。避免创建过多不必要的对象,对于可复用的对象,采用对象池技术来管理,减少频繁的对象创建和销毁开销。例如,在图片加载中,使用缓存机制,将已经加载过的图片缓存起来,下次需要时直接从缓存中获取,而不是重新加载,这样能有效减少内存占用。

其次,优化布局结构。避免过度嵌套的布局,因为复杂的布局会增加视图层次,导致渲染时占用更多内存和时间。可以通过合并视图、使用更高效的布局方式来简化布局结构。比如,能用线性布局实现的效果,就尽量不用相对布局,因为相对布局的测量和布局过程相对复杂。

再者,关注内存泄漏问题。仔细检查代码中是否存在持有对象引用但长时间不释放的情况,比如注册的事件监听器、广播接收器等没有及时取消注册,导致相关对象无法被回收。使用 LeakCanary 等工具来检测内存泄漏,及时发现并解决问题。

另外,对于大内存对象的使用要格外谨慎。如大图片的加载,要进行适当的压缩和尺寸调整,根据图片显示的区域大小来加载合适分辨率的图片,避免加载过高分辨率的图片造成内存浪费。同时,在使用完大内存对象后,要及时将其置为null,让垃圾回收器能够回收其占用的内存。

LeakCanary 的原理是什么?弱引用和软引用有什么区别?

LeakCanary 是一款用于检测 Android 应用内存泄漏的工具。其原理是基于 Java 的可达性分析算法。当 Activity 或其他对象不再被使用时,LeakCanary 会通过弱引用持有该对象,并在后台线程中检查这个弱引用是否被回收。如果在一定时间后,弱引用没有被回收,说明该对象可能发生了内存泄漏,LeakCanary 会通过分析引用链来找出导致内存泄漏的原因,并给出相应的提示。

弱引用和软引用都是 Java 中用于处理对象引用的机制。弱引用的特点是,当垃圾回收器扫描到只被弱引用关联的对象时,无论当前内存是否充足,都会回收该对象。比如,在一个缓存系统中,使用弱引用存储缓存对象,当系统内存不足时,这些缓存对象会被优先回收,以释放内存。而软引用则不同,当垃圾回收器发现只有软引用指向某个对象时,如果当前内存空间足够,就不会回收该对象;只有当内存不足时,才会回收软引用关联的对象。例如,在图片缓存中,可以使用软引用来存储图片数据,这样在内存充足时,图片可以一直保存在缓存中,提高访问效率,当内存紧张时,图片又能被回收,避免内存溢出。

你在项目中是否有过性能优化的经历?请分享一下你的经验。

在实际项目中,有过不少性能优化的经历。比如,在一个图片展示类的应用中,最初加载图片时,存在卡顿和内存占用过高的问题。首先,对图片加载进行了优化。使用了 Glide 等图片加载框架,并配置了合适的缓存策略,包括内存缓存和磁盘缓存。根据图片的使用频率和重要性,设置不同的缓存级别,对于经常使用的图片,优先从内存缓存中获取,提高加载速度。同时,对图片进行了压缩处理,根据图片显示的大小和分辨率,在加载前将图片压缩到合适的尺寸,减少内存占用。

其次,对布局进行了优化。检查发现部分界面存在过度嵌套的布局,导致视图渲染时间过长。通过将一些线性布局和相对布局进行合并和调整,减少了视图层次,提高了布局的绘制效率。例如,将一些可以使用FrameLayout来实现的布局,替换了原来复杂的多层嵌套布局,使界面的加载速度明显提升。

另外,还对内存泄漏问题进行了排查和处理。使用 LeakCanary 工具,发现了一些由于未及时取消注册事件监听器和广播接收器导致的内存泄漏。在相关页面销毁时,及时取消了这些注册,避免了对象被长时间持有而无法回收,有效降低了内存泄漏的风险,提高了应用的稳定性。

TCP/IP 五层模型是什么?

TCP/IP 五层模型是一种将计算机网络体系结构进行分层的模型,从下到上依次为物理层、数据链路层、网络层、传输层和应用层。

物理层负责处理物理介质上的信号传输,比如电缆、光纤等传输介质中的电信号、光信号等。它定义了物理设备的电气特性、机械特性等,确保数据能够在物理介质上进行正确的传输。

数据链路层主要负责将物理层接收到的信号转换为数据帧,并进行差错检测和纠正。它还负责在相邻节点之间进行数据帧的传输,通过 MAC 地址来识别不同的设备。例如,以太网协议就是数据链路层的一种常见协议。

网络层的作用是实现不同网络之间的数据路由和寻址。它通过 IP 地址来标识不同的主机和网络,使用路由算法来确定数据从源节点到目标节点的最佳路径。比如,路由器就是工作在网络层的设备,负责根据 IP 地址转发数据包。

传输层主要为应用程序提供端到端的通信服务。它通过端口号来区分不同的应用程序,常见的协议有 TCP 和 UDP。TCP 协议提供可靠的、面向连接的传输服务,保证数据的完整性和顺序性;UDP 协议则提供不可靠的、无连接的传输服务,适用于对实时性要求较高但对数据准确性要求相对较低的场景,如视频流传输。

应用层是直接面向用户的一层,包含了各种应用程序和协议,如 HTTP、FTP、SMTP 等。这些协议规定了应用程序如何与网络进行交互,以实现各种功能,如网页浏览、文件传输、电子邮件发送等。

为什么 TCP 两次握手不安全?

TCP 是一种面向连接的传输层协议,在建立连接时通常采用三次握手的方式。如果采用两次握手,会存在一些安全问题。

在两次握手的情况下,假设客户端发送一个连接请求报文段到服务器,由于网络延迟等原因,这个报文段在网络中滞留了一段时间。然后客户端以为服务器没有收到请求,又重新发送了一个连接请求。服务器收到第二个请求后,发送确认报文并建立连接。此时,客户端和服务器可以正常通信,通信结束后连接被释放。

然而,之前那个滞留的请求报文段后来又到达了服务器,服务器会认为这是客户端发起的新连接请求,于是发送确认报文并准备建立连接。但客户端此时已经没有发起新连接的意愿,它会忽略服务器的确认报文。这样一来,服务器就会一直处于等待客户端发送数据的状态,造成资源浪费。

另外,两次握手无法可靠地保证双方都具备接收和发送数据的能力。在三次握手中,客户端在收到服务器的确认后,还会向服务器发送一个确认报文,服务器收到这个报文后才能确定客户端具备接收和发送数据的能力。而两次握手缺少了客户端的最后一次确认,服务器无法确定客户端是否能正常接收数据,可能导致数据传输出现问题。因此,为了保证连接的可靠性和安全性,TCP 采用三次握手而不是两次握手。

HTTPS 是如何加密数据的?

HTTPS(超文本传输安全协议)是在 HTTP 的基础上加入了加密和认证机制,用于保障数据在网络传输过程中的安全性。它主要通过对称加密、非对称加密以及数字证书等技术来实现数据的加密。

首先,在 HTTPS 通信过程中,会使用到数字证书。数字证书是由权威的证书颁发机构(CA)签发的,包含了服务器的公钥、证书的有效期、服务器的域名等信息。客户端在与服务器建立连接时,服务器会将自己的数字证书发送给客户端。客户端会验证数字证书的有效性,比如检查证书是否过期、证书是否由信任的 CA 签发等。

然后,会采用非对称加密算法(如 RSA)来交换对称加密的密钥。非对称加密使用一对密钥,即公钥和私钥。服务器将自己的公钥包含在数字证书中发送给客户端。客户端生成一个对称加密的密钥(如 AES 算法的密钥),使用服务器的公钥对这个对称密钥进行加密,然后发送给服务器。服务器使用自己的私钥解密得到对称密钥。

对称加密算法(如 AES)具有高效性,在数据传输阶段,会使用对称加密来加密实际传输的数据。因为对称加密使用相同的密钥进行加密和解密,所以在数据传输过程中,客户端和服务器都使用之前协商好的对称密钥对数据进行加密和解密。

在数据传输过程中,还会使用到消息认证码(MAC)来保证数据的完整性。客户端和服务器会根据加密后的数据计算出 MAC 值,并将其随数据一起传输。接收方在收到数据后,也计算出 MAC 值,然后与接收到的 MAC 值进行比较,以确保数据在传输过程中没有被篡改。

okhttp 拦截器的作用是什么,如何使用?

OkHttp 拦截器是 OkHttp 库中非常重要的一个功能组件,它可以对请求和响应进行拦截和处理。

拦截器的作用主要有以下几个方面:

  • 日志记录:可以拦截请求和响应,将请求的 URL、请求头、请求体以及响应的状态码、响应头、响应体等信息进行记录,方便开发者了解网络请求的详细情况,排查问题。
  • 请求头和响应头的修改:在请求发送之前,可以添加、修改或删除请求头信息;在响应接收之后,可以对响应头进行处理,比如根据响应头的信息进行一些业务逻辑的判断。
  • 请求体和响应体的处理:可以对请求体进行修改,例如对请求数据进行加密处理;对响应体进行解析和处理,比如对响应数据进行解密、格式转换等。
  • 重试机制:当请求失败时,可以通过拦截器实现重试逻辑,提高请求的成功率。

使用拦截器的步骤如下:

  1. 创建拦截器:可以实现Interceptor接口,重写intercept方法。在intercept方法中,可以获取到Chain对象,通过Chain对象获取请求和响应信息。例如:

Interceptor logInterceptor = new Interceptor() {@Overridepublic Response intercept(Chain chain) throws IOException {Request request = chain.request();// 记录请求信息System.out.println("请求URL: " + request.url());Response response = chain.proceed(request);// 记录响应信息System.out.println("响应状态码: " + response.code());return response;}
};

  1. 添加拦截器到 OkHttp 客户端:在创建 OkHttp 客户端时,通过OkHttpClient.BuilderaddInterceptor方法添加拦截器。例如:

OkHttpClient client = new OkHttpClient.Builder().addInterceptor(logInterceptor).build();

另外,还有Chainproceed方法非常关键,它会将请求传递给下一个拦截器或者最终的服务器,并返回响应。如果在拦截器中不调用proceed方法,请求将不会被发送,响应也不会被接收。

MVP、MVC 和 MVVM 模式是什么?

MVC(Model-View-Controller)模式
MVC 是一种比较早的软件架构模式。Model 代表数据和业务逻辑,它负责处理数据的获取、存储和更新等操作。例如,在一个电商应用中,Model 可能包括商品数据的获取、订单的处理等逻辑。View 负责展示数据,它是用户界面的部分,直接与用户进行交互。Controller 作为 Model 和 View 之间的桥梁,它接收用户在 View 上的操作,然后调用 Model 的方法进行业务处理,再根据处理结果更新 View。比如,用户在 View 上点击一个按钮,Controller 会获取点击事件,调用 Model 的相关方法,然后更新 View 显示的内容。MVC 模式的优点是实现了数据和视图的分离,便于维护和扩展;缺点是 Controller 层可能会变得复杂,因为它需要处理大量的业务逻辑和视图交互逻辑。

MVP(Model-View-Presenter)模式
MVP 是在 MVC 基础上发展而来的。Model 同样负责数据和业务逻辑。View 只负责展示数据和接收用户操作,不包含任何业务逻辑。Presenter 作为中间层,它从 Model 获取数据,然后将数据处理后传递给 View。当用户在 View 上进行操作时,View 会通知 Presenter,Presenter 再调用 Model 的方法进行业务处理,最后更新 View。例如,在一个新闻应用中,Presenter 会从 Model 获取新闻数据,处理后传递给 View 显示;用户在 View 上点击刷新按钮,View 通知 Presenter,Presenter 调用 Model 获取最新新闻数据并更新 View。MVP 模式进一步解耦了 View 和 Model,使得代码更易于测试和维护;但可能会导致 Presenter 层变得臃肿,因为它需要处理大量的业务逻辑和与 View 的交互逻辑。

MVVM(Model-View-ViewModel)模式
MVVM 是一种比较新的架构模式。Model 还是负责数据和业务逻辑。View 是用户界面,与用户进行交互。ViewModel 是 MVVM 模式的核心,它通过数据绑定技术将 View 和 Model 连接起来。当 Model 中的数据发生变化时,ViewModel 会自动更新 View;当用户在 View 上进行操作时,ViewModel 会更新 Model。例如,在一个表单应用中,ViewModel 会将表单数据与 Model 进行绑定,当用户输入数据时,ViewModel 会将数据同步到 Model;当 Model 中的数据发生变化时,View 会自动更新显示。MVVM 模式实现了数据的双向绑定,提高了开发效率和用户体验;缺点是学习成本较高,需要掌握数据绑定等相关技术。

实现一个二叉树的层序遍历。(java 代码完整实现)

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;class TreeNode {int val;TreeNode left;TreeNode right;TreeNode(int val) {this.val = val;this.left = null;this.right = null;}
}public class BinaryTreeLevelOrderTraversal {public static List<List<Integer>> levelOrder(TreeNode root) {List<List<Integer>> result = new ArrayList<>();if (root == null) {return result;}Queue<TreeNode> queue = new LinkedList<>();queue.add(root);while (!queue.isEmpty()) {int size = queue.size();List<Integer> level = new ArrayList<>();for (int i = 0; i < size; i++) {TreeNode node = queue.poll();level.add(node.val);if (node.left != null) {queue.add(node.left);}if (node.right != null) {queue.add(node.right);}}result.add(level);}return result;}public static void main(String[] args) {TreeNode root = new TreeNode(3);root.left = new TreeNode(9);root.right = new TreeNode(20);root.right.left = new TreeNode(15);root.right.right = new TreeNode(7);List<List<Integer>> traversalResult = levelOrder(root);for (List<Integer> level : traversalResult) {System.out.println(level);}}
}

上述代码中,首先定义了一个TreeNode类来表示二叉树的节点。然后在BinaryTreeLevelOrderTraversal类中,levelOrder方法实现了二叉树的层序遍历。使用一个队列来存储节点,先将根节点入队。在循环中,每次获取队列当前层的节点数量,依次出队节点,将节点值加入当前层的列表中,并将节点的左右子节点(如果存在)入队。最后将每一层的列表加入结果列表中。在main方法中,创建一个简单的二叉树,并调用levelOrder方法进行层序遍历,将结果打印出来。

Android 如果想添加一个虚拟号码跳转通讯录的功能,如何实现?

要在 Android 中实现添加虚拟号码跳转通讯录的功能,可以按照以下步骤进行:

  1. 获取权限:在 AndroidManifest.xml 文件中添加必要的权限,主要是读写通讯录的权限。

<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />

同时,在运行时需要动态申请这些权限。在 Activity 中可以使用ActivityCompat.requestPermissions方法来申请权限,并在onRequestPermissionsResult方法中处理权限申请的结果。

  1. 创建虚拟号码数据:可以定义一个虚拟号码的字符串变量,例如:

String virtualNumber = "1234567890";

  1. 跳转通讯录并添加号码:使用Intent来启动通讯录应用,并传递要添加的号码数据。可以使用Intent.ACTION_INSERT_OR_EDIT来实现添加或编辑联系人的功能。示例代码如下:

Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
intent.setType(ContactsContract.RawContacts.CONTENT_TYPE);
intent.putExtra(ContactsContract.Intents.Insert.PHONE, virtualNumber);
startActivity(intent);

上述代码创建了一个Intent,设置其动作为ACTION_INSERT_OR_EDIT,类型为ContactsContract.RawContacts.CONTENT_TYPE,并将要添加的虚拟号码作为电话号码附加到Intent中。然后通过startActivity方法启动通讯录应用,此时通讯录应用会显示添加联系人的界面,并自动填充虚拟号码。

  1. 处理联系人添加成功或失败的情况:可以使用startActivityForResult方法来启动通讯录应用,然后在onActivityResult方法中处理返回结果。例如,当用户成功添加联系人后,通讯录应用会返回一个结果码,在onActivityResult方法中可以根据结果码判断添加是否成功,并进行相应的处理。

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {super.onActivityResult(requestCode, resultCode, data);if (resultCode == RESULT_OK) {// 联系人添加成功的处理逻辑} else {// 联系人添加失败的处理逻辑}
}

需要注意的是,不同的 Android 版本和设备可能会对通讯录操作有一些差异,在实际开发中需要进行充分的测试,确保功能的稳定性和兼容性。

http://www.xdnf.cn/news/419563.html

相关文章:

  • React面试常问问题详解
  • AJAX 简介
  • 经典中的经典-比特币白皮书中文版
  • 【RabbitMQ】七种工作模式介绍
  • day19-线性表(顺序表)(链表)
  • 里氏替换原则:Java 面向对象设计的基石法则
  • langchain学习
  • nvidia驱动更新-先卸载再安装-ubuntu
  • Jsp技术入门指南【十三】基于 JSTL SQL 标签库实现 MySQL 数据库连接与数据分页展示
  • 解锁课程编辑器之独特风姿
  • pdf url 转 图片
  • loki grafana 页面查看 loki 日志偶发 too many outstanding requests
  • 基于大模型预测的视神经脊髓炎技术方案大纲
  • Flannel UDP 模式的优缺点
  • MySQL的Docker版本,部署在ubantu系统
  • Starrocks的主键表涉及到的MOR Delete+Insert更新策略
  • 2025年化学工程与材料物理国际会议(CEMP 2025)
  • [学习] RTKLib详解:qzslex.c、rcvraw.c与solution.c
  • 移动端前端开发调试工具/webkit调试工具/小程序调试工具WebDebugX使用教程
  • OpenCV的CUDA模块进行图像处理
  • 文件相关操作
  • 通过QPS和并发数定位问题
  • 网络体系结构(OSI,TCP/IP)
  • 3.4 数字特征
  • 关于网站提交搜索引擎
  • 【Cesium入门教程】第七课:Primitive图元
  • 算法备案部分咨询问题解答第三期
  • leetcode-hot-100 (滑动窗口)
  • Windows部署LatentSync唇形同步(字节跳动北京交通大学联合开源)
  • 【Redis 进阶】缓存