Android面试题整理

47 阅读1小时+

java部分

一. jvm垃圾回收

1. jvm结构

程序计数器

程序计数器(Program Counter Register)  是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。

字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于 Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令

因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

  • 如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址
  • 如果线程正在执行的是一个 Native 方法,这个计数器值则为空(Undefined)。

此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

Java 虚拟机栈

Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)  用于存储局部变量表、操作数栈、动态链接、方法出口等消息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和 returnAddress 类型(指向了一条字节码指令的地址)。

其中 64 位长度的 long 和 double 类型的数据会占用两个局部变量空间(Slot),其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

在 Java 虚拟机规范中,对这个区域规定了两种异常状态:

  • 如果线程请求的栈深度大于虚拟机所允许的的深度,将抛出 StackOverflowError 异常。
  • 如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。

本地方法栈

本地方法栈(Native Method Stack)  与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。

在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(例如:Sun HotSpot虚拟机)直接就把虚拟机栈和本地方法栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。

Java 堆

对于大多数应用来说,Java 堆(Java Heap)  是 Java 虚拟机所管理的的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

Java堆是垃圾收集器管理的主要区域,从内存回收的角度来看,由于现在收集器基本采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有 Eden 空间、From Survivor 空间、To Survivor 空间等。

从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。不过无论如何划分,都与存放内容无关,无论哪个区域,存储的仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。

方法区

方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

运行时常量池(Runtime Constant Pool)  是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table) ,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时就会抛出 OutOfMemoryError 异常。

2. 如何进行回收的

  • Eden 区

大多数情况下,对象会在新生代 Eden 区中进行分配,当 Eden 区没有足够空间进行分配时,虚拟机会发起一次 Minor GC,Minor GC 相比 Major GC 更频繁,回收速度也更快。 通过 Minor GC 之后,Eden 会被清空,Eden 区中绝大部分对象会被回收,而那些无需回收的存活对象,将会进到 Survivor 的 From 区(若 From 区不够,则直接进入 Old 区)。

  • Survivor 区

Survivor 区相当于是 Eden 区和 Old 区的一个缓冲,类似于我们交通灯中的黄灯。Survivor 又分为2个区,一个是 From 区,一个是 To 区。每次执行 Minor GC,会将 Eden 区和 From 存活的对象放到 Survivor 的 To 区(如果 To 区不够,则直接进入 Old 区)。Survivor 的存在意义就是减少被送到老年代的对象,进而减少 Major GC 的发生。Survivor 的预筛选保证,只有经历16次 Minor GC 还能在新生代中存活的对象,才会被送到老年代。

  • Old 区

老年代占据着2/3的堆内存空间,只有在 Major GC 的时候才会进行清理,每次 GC 都会触发“Stop-The-World”。内存越大,STW 的时间也越长,所以内存也不仅仅是越大就越好。由于复制算法在对象存活率较高的老年代会进行很多次的复制操作,效率很低,所以老年代这里采用的是标记——整理算法。

二. java特性

1. 多态?

在 Java 中,多态是指一个类的对象可以被当作其父类的对象来使用的能力。多态的核心在于继承和方法重写。通过继承,子类可以继承父类的方法和属性;通过方法重写,子类可以覆盖父类的方法,从而实现不同的行为。

public class Animal {
    public void makeSound() {
        System.out.println("Some sound");
    }
}
public class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Woof!");
    }
}

public class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Meow!");
    }
}
public class Main {
    public static void main(String[] args) {
        // 创建子类对象
        Dog dog = new Dog();
        Cat cat = new Cat();

        // 向上转型
        Animal animal1 = dog;
        Animal animal2 = cat;

        // 调用方法
        animal1.makeSound(); // 输出 "Woof!"
        animal2.makeSound(); // 输出 "Meow!"

        // 方法重写
        makeSound(animal1); // 输出 "Woof!"
        makeSound(animal2); // 输出 "Meow!"
    }

    // 方法接受 Animal 类型的参数
    public static void makeSound(Animal animal) {
        animal.makeSound();
    }
}

多态:子类对象可以赋值给父类引用,从而实现多态。 方法重写:子类可以重写父类的方法,提供不同的实现。 向上转型:子类对象可以赋值给父类引用,从而实现多态调用。 通过这种方式,多态使得代码更加灵活和可扩展,能够更好地应对不同类型对象的行为差异。

接口(Interface) 定义: 接口是一种完全抽象的类,它只能包含抽象方法(默认为 public abstract)、默认方法(default)、静态方法(static)和常量(默认为 public static final)。 特点: 接口中的所有方法默认是 public abstract 的。 接口可以包含默认方法(default)和静态方法(static)。 接口中不能有实例变量,只有常量。 一个类可以实现多个接口,实现接口时必须实现接口中的所有抽象方法。 接口主要用于定义行为规范,而不是具体的实现细节。 抽象类(Abstract Class) 定义: 抽象类是一种部分抽象的类,它可以包含抽象方法和具体方法。 抽象类可以包含实例变量、构造器和其他成员。 特点: 抽象类中的方法可以是抽象的(abstract)也可以是非抽象的(具体实现)。 抽象类可以包含实例变量、构造器和其他成员。 一个类只能继承一个抽象类。 抽象类主要用于提供通用的实现和行为规范。

3. 集合

1. hashmap

  1. 什么是hashmap,如何扩容的,为什么扩容的长度是2的幂 hashMap 基于 hashing 原理,我们通过 put() 和 get() 方法储存和获取对象。当我们将键值对传递给 put() 方法时,它调用键对象的 hashCode() 方法来计算 hashcode,然后找到 bucket 位置来储存 Entry 对象。当两个对象的 hashcode 相同时,它们的 bucket 位置相同,‘碰撞’会发生。因为 HashMap 使用链表存储对象,这个 Entry 会存储在链表中,当获取对象时,通过键对象的 equals() 方法找到正确的键值对,然后返回值对象。

特点 线程不安全:HashMap 不是线程安全的,如果在多线程环境中使用,需要外部同步。 允许空键和空值:HashMap 允许一个空键和多个空值。 性能:通常情况下,HashMap 的性能优于 Hashtable,因为它没有进行额外的同步操作。 迭代顺序:HashMap 不保证迭代顺序,即每次遍历的顺序可能不同。

1. Hashtable

线程安全:Hashtable 是线程安全的,所有的方法都是同步的。 不允许空键和空值:Hashtable 不允许空键和空值,尝试插入空键或空值会抛出 NullPointerException。 性能:由于同步操作,Hashtable 的性能通常低于 HashMap。 迭代顺序:Hashtable 也不保证迭代顺序

1. HashSet

无序集合:HashSet 是一个无序集合,不保证元素的插入顺序。 不允许重复元素:HashSet 不允许重复元素,每个元素都是唯一的。 基于 HashMap 实现:HashSet 内部使用 HashMap 来存储元素,键为元素本身,值为 PRESENT(一个静态对象)。 线程不安全:HashSet 不是线程安全的,如果在多线程环境中使用,需要外部同步。

SparseMap

2. currentHashMap

在 Java 7 及之前版本中,ConcurrentHashMap 使用分段锁(Segment)机制来实现并发控制。每个 Segment 实际上是一个小型的哈希表,每个 Segment 有自己的锁,这样可以允许多个线程同时访问不同的 Segment,从而提高并发性能。 然而,在 Java 8 中,ConcurrentHashMap 移除了 Segment 机制,改用 CAS(Compare and Swap)操作和 synchronized 锁来实现更细粒度的并发控制。这种变化使得 ConcurrentHashMap 在低并发场景下表现更好,同时在高并发场景下依然保持高性能。

  1. CAS 操作 Java 8 中的 ConcurrentHashMap 广泛使用了 CAS 操作来实现无锁化更新。CAS 操作可以在不使用锁的情况下,原子地更新变量的值。这减少了锁的竞争,提高了并发性能。

3. ArrayMap

ArrayMap 内部使用数组来存储键值对,而不是像 HashMap 那样使用哈希表。这使得 ArrayMap 在键的数量较少时特别有效

特点和优势 内存效率:ArrayMap 在键的数量较少时比 HashMap 更节省内存。 性能:在键的数量较少时,ArrayMap 的查找、插入和删除操作通常比 HashMap 更快。 有序性:ArrayMap 保持插入顺序,而 HashMap 不保证顺序。

注意事项 容量限制:ArrayMap 在键的数量较多时性能会下降,因为它内部使用数组进行存储和查找。 线程安全:ArrayMap 不是线程安全的,多线程环境下需要外部同步。

4. ArrayList 和 LinkedList

ArrayList: 基于动态数组实现。 元素存储在连续的内存空间中。 支持随机访问。

LinkedList: 基于双向链表实现。 每个元素(节点)包含前驱和后继指针。 不支持随机访问,但插入和删除操作效率较高。

查询复杂度 ArrayList: 随机访问:O(1) 线性搜索:O(n) LinkedList: 随机访问:O(n) 线性搜索:O(n)

空间复杂度 ArrayList: 存储 n 个元素的空间复杂度为 O(n)。 额外的空间开销主要用于数组的扩容机制。 LinkedList: 存储 n 个元素的空间复杂度为 O(n)。 每个节点除了存储数据外,还需要存储前驱和后继指针,因此实际占用的空间比 ArrayList 大。

总结 ArrayList 适合需要频繁随机访问的场景。 LinkedList 适合需要频繁插入和删除操作的场景

ArrayList和LinkedList怎么动态扩容的吗?

ArrayList:

ArrayList 的初始大小是0,然后,当add第一个元素的时候大小则变成10。并且,在后续扩容的时候会变成当前容量的1.5倍大小。

LinkedList:

linkedList 是一个双向链表,没有初始化大小,也没有扩容的机制,就是一直在前面或者后面新增就好。

LinkedList、ArrayList、HashSet是非线程安全的,Vector是线程安全的;

HashMap是非线程安全的,HashTable是线程安全的;

StringBuilder是非线程安全的,StringBuffer是线程安的。

4. 线程

线程的几种状态

  1. NEW 描述:线程被创建但尚未启动。 特点:此时线程还没有调用 start() 方法。

  2. RUNNABLE 描述:线程正在 JVM 中执行,可能正在运行或准备运行。 特点:线程已经调用了 start() 方法,并且正在等待 CPU 资源来执行其代码。它也可能正在执行 I/O 操作或其他阻塞操作,但仍然被认为是可运行的。

  3. BLOCKED 描述:线程被阻塞,等待获取监视器锁以进入同步块或方法。 特点:当一个线程试图进入一个由其他线程持有的同步块时,它会进入 BLOCKED 状态,直到它获得锁。 (synchronized, await)

  4. WAITING 描述:线程无限期等待另一个线程执行特定动作。 特点:通常通过调用 Object.wait()、Thread.join() 或 LockSupport.park() 进入此状态。

  5. TIMED_WAITING 描述:线程等待另一个线程执行特定动作,但在指定时间内等待。 特点:通常通过调用 Thread.sleep()、Object.wait(long)、Thread.join(long) 或 LockSupport.parkNanos() 等方法进入此状态。

  6. TERMINATED 描述:线程已经结束执行,要么正常完成,要么由于未捕获的异常而终止。 特点:线程的任务已完成,不再活动。

    1. sleep 和wait的区别
  • wait() / notify() / notifyAll()

wait()notify()notifyAll() 是定义在Object类的实例方法,用于控制线程状态,三个方法都必须在synchronized 同步关键字所限定的作用域中调用,否则会报错 java.lang.IllegalMonitorStateException

方法说明
wait()线程状态由 的使用权。Running 变为 Waiting, 并将当前线程放入等待队列中
notify()notify() 方法是将等待队列中一个等待线程从等待队列移动到同步队列中
notifyAll()则是将所有等待队列中的线程移动到同步队列中

被移动的线程状态由 Running 变为 Blocked,notifyAll 方法调用后,等待线程依旧不会从 wait() 返回,需要调用 notify() 或者 notifyAll() 的线程释放掉锁后,等待线程才有机会从 wait() 返回。

  • join() / sleep() / yield()

在很多情况,主线程创建并启动子线程,如果子线程中需要进行大量的耗时计算,主线程往往早于子线程结束。这时,如果主线程想等待子线程执行结束之后再结束,比如子线程处理一个数据,主线程要取得这个数据,就要用 join() 方法。

sleep(long) 方法在睡眠时不释放对象锁,而 join() 方法在等待的过程中释放对象锁。

yield() 方法会临时暂停当前正在执行的线程,来让有同样优先级的正在等待的线程有机会执行。如果没有正在等待的线程,或者所有正在等待的线程的优先级都比较低,那么该线程会继续运行。执行了yield方法的线程什么时候会继续运行由线程调度器来决定。

5. 线程池

参数

corePoolSize:核心线程数,线程池中始终保持的最小线程数。 maximumPoolSize:最大线程数,线程池中允许的最大线程数。 keepAliveTime:线程空闲时间,当线程数大于核心线程数时,多余的空闲线程在等待新任务时的最长时间。 unit:keepAliveTime的时间单位。 workQueue:任务队列,用于存放等待执行的任务。 threadFactory:线程工厂,用于创建新线程。 handler:拒绝策略,当任务无法被添加到线程池时的处理策略。 区别总结

有几种线程池生命方式,什么作用

FixedThreadPool:固定大小,适合处理大量耗时任务。 SingleThreadExecutor:单线程,保证任务顺序执行。 CachedThreadPool:动态调整,适合处理大量短小任务。 ScheduledThreadPool:支持定时任务,适合定期执行任务。 WorkStealingPool:工作窃取,适合并行处理大量任务。

当线程数大于等于核心线程数但小于最大线程数时,如果工作队列还没有满,新的任务会被放入工作队列中,而不是立即创建新的非核心线程。只有当工作队列已满且线程数仍小于最大线程数时,才会创建新的非核心线程来处理新任务。 详细解释 线程数小于核心线程数: 如果当前线程数小于核心线程数,线程池会立即创建新的核心线程来处理新任务,即使其他工作线程是空闲的。 线程数大于等于核心线程数但小于最大线程数: 如果当前线程数已经达到核心线程数,但小于最大线程数: 工作队列未满:新的任务会被放入工作队列中,等待现有的线程从队列中取出并执行。 工作队列已满:线程池会创建新的非核心线程来处理新任务,直到线程数达到最大线程数。 线程数达到最大线程数: 如果当前线程数已经达到最大线程数,且工作队列也已满,线程池会调用拒绝策略来处理新任务

拒绝策略 AbortPolicy:默认策略,抛出 RejectedExecutionException 异常。 CallerRunsPolicy:由调用者线程执行被拒绝的任务。 DiscardPolicy:直接丢弃被拒绝的任务。 DiscardOldestPolicy:丢弃队列中最老的任务,然后重新尝试提交被拒绝的任务。 自定义拒绝策略:实现 RejectedExecutionHandler 接口,自定义处理逻辑。

6. 锁

volatile: 确保内存可见性和禁止指令重排序。 不能保证复合操作的原子性(例如 i++)。 适用于简单的读写操作。 synchronized: 确保内存可见性和互斥性。 可以保证复合操作的原子性。 适用于复杂的同步操作。

2、Synchronized优化后的锁机制简单介绍一下,包括自旋锁、偏向锁、轻量级锁、重量级锁?

自旋锁:

线程自旋说白了就是让cpu在做无用功,比如:可以执行几次for循环,可以执行几条空的汇编指令,目的是占着CPU不放,等待获取锁的机会。如果旋的时间过长会影响整体性能,时间过短又达不到延迟阻塞的目的。

偏向锁

偏向锁就是一旦线程第一次获得了监视对象,之后让监视对象“偏向”这个线程,之后的多次调用则可以避免CAS操作,说白了就是置个变量,如果发现为true则无需再走各种加锁/解锁流程。

轻量级锁:

轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁竞争用的时候,偏向锁就会升级为轻量级锁;

重量级锁

重量锁在JVM中又叫对象监视器(Monitor),它很像C中的Mutex,除了具备Mutex(0|1)互斥的功能,它还负责实现了Semaphore(信号量)的功能,也就是说它至少包含一个竞争锁的队列,和一个信号阻塞队列(wait队列),前者负责做互斥,后一个用于做线程同步。

synchronized关键字和Lock的区别你知道吗?为什么Lock的性能好一些?

类别synchronizedLock(底层实现主要是Volatile + CAS)
存在层次Java的关键字,在jvm层面上是一个类
锁的释放1、已获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁。在finally中必须释放锁,不然容易造成线程死锁。
锁的获取假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待。分情况而定,Lock有多个锁获取的方式,大致就是可以尝试获得锁,线程可以不用一直等待
锁状态无法判断可以判断
锁类型可重入 不可中断 非公平可重入 可判断 可公平(两者皆可)
性能少量同步大量同步

Lock(ReentrantLock)的底层实现主要是Volatile + CAS(乐观锁),而Synchronized是一种悲观锁,比较耗性能。但是在JDK1.6以后对Synchronized的锁机制进行了优化,加入了偏向锁、轻量级锁、自旋锁、重量级锁,在并发量不大的情况下,性能可能优于Lock机制。所以建议一般请求并发量不大的情况下使用synchronized关键字。

2. 死锁是什么

    private final Object lock1 = new Object();
    private final Object lock2 = new Object();

    public void method1() {
        synchronized (lock1) {
            System.out.println("Method 1 acquired lock1");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock2) {
                System.out.println("Method 1 acquired lock2");
            }
        }
    }

    public void method2() {
        synchronized (lock2) {
            System.out.println("Method 2 acquired lock2");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock1) {
                System.out.println("Method 2 acquired lock1");
            }
        }
    }

    public static void main(String[] args) {
        DeadlockExample example = new DeadlockExample();

        new Thread(() -> example.method1()).start();
        new Thread(() -> example.method2()).start();
    }
}

在这个例子中,method1 和 method2 分别尝试以不同的顺序获取 lock1 和 lock2,可能会导致死锁。具体过程如下: 线程1 调用 method1,获取 lock1,然后休眠 100 毫秒。 线程2 调用 method2,获取 lock2,然后休眠 100 毫秒。 线程1 试图获取 lock2,但 lock2 被 线程2 持有,因此 线程1 等待。 线程2 试图获取 lock1,但 lock1 被 线程1 持有,因此 线程2 等待。 线程1 和 线程2 互相等待,形成死锁。

7. equal ==

== 的默认行为是直接比较内存地址。 equals 的默认行为也是比较内存地址,但很多类(如 String、Integer 等)重写了 equals 方法,以便根据对象的实际内容进行比较。

8. htttp https

HTTPS 为了兼顾安全与效率,同时使用了对称加密和非对称加密。数据是被对称加密传输的,对称加密过程需要客户端的一个密钥,为了确保能把该密钥安全传输到服务器端,采用非对称加密对该密钥进行加密传输,总的来说,对数据进行对称加密,对称加密所要使用的密钥通过非对称加密传输。

尽管中间人可以拦截和转发服务端的证书,但它无法完成整个握手过程,因为: 证书验证:客户端会验证证书的有效性,中间人无法伪造受信任的证书。 预主密钥的加密:中间人没有服务端的私钥,无法解密客户端发送的加密预主密钥。 主密钥和会话密钥的生成:中间人无法生成正确的主密钥和会话密钥,因此无法解密或加密后续的数据传输。 消息认证码:中间人无法生成正确的 MAC,因此无法伪造 Finished 消息。 通过这些机制,HTTPS 协议确保了即使在存在中间人的情况下,数据传输仍然是安全的

4.1 客户端发起连接 过程: 客户端发送一个 ClientHello 消息,其中包含客户端支持的协议版本、加密套件列表和 Client Random。 4.2 服务器响应 过程: 服务器选择一个加密套件,并发送一个 ServerHello 消息,其中包含选定的加密套件、Server Random 和服务器证书。 服务器可能还会发送 CertificateRequest 消息,要求客户端提供证书(用于双向认证)。 4.3 客户端验证证书 过程: 客户端验证服务器证书的有效性,包括证书的签名、有效期和域名匹配等。 如果验证通过,客户端生成一个预主密钥(Pre-Master Secret),并使用服务器证书中的公钥对其进行加密,然后发送给服务器。 4.4 服务器解密预主密钥 过程: 服务器使用私钥解密预主密钥。 4.5 生成主密钥 过程: 客户端和服务器分别使用 Client Random、Server Random 和预主密钥,通过密钥派生函数(如 PRF)生成主密钥(Master Secret)。 4.6 生成会话密钥 过程: 客户端和服务器使用主密钥和随机数生成会话密钥(Session Keys),用于后续的数据加密和解密。 4.7 完成握手 过程: 客户端发送 ChangeCipherSpec 消息,表示后续消息将使用会话密钥进行加密。 客户端发送 Finished 消息,包含之前所有握手消息的哈希值,以验证握手过程的完整性。 服务器发送 ChangeCipherSpec 消息,表示后续消息将使用会话密钥进行加密。 服务器发送 Finished 消息,包含之前所有握手消息的哈希值,以验证握手过程的完整性。

9. 三次握手 四次挥手

所谓三次握手(Three-way Handshake),是指建立一个 TCP 连接时,需要客户端和服务器总共发送3个包。

三次握手的目的是连接服务器指定端口,建立 TCP 连接,并同步连接双方的序列号和确认号,交换 TCP 窗口大小信息。在 socket 编程中,客户端执行 connect() 时。将触发三次握手。

  • 第一次握手(SYN=1, seq=x):

客户端发送一个 TCP 的 SYN 标志位置 1 的包,指明客户端打算连接的服务器的端口,以及初始序号 X,保存在包头的序列号 (Sequence Number) 字段里。

发送完毕后,客户端进入 SYN_SEND 状态。

  • 第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1):

服务器发回确认包(ACK)应答。即 SYN 标志位和 ACK 标志位均为 1。服务器端选择自己 ISN 序列号,放到 Seq 域里,同时将确认序号(Acknowledgement Number)设置为客户的 ISN 加1,即 X+1。 发送完毕后,服务器端进入 SYN_RCVD 状态。

  • 第三次握手(ACK=1,ACKnum=y+1)

客户端再次发送确认包(ACK),SYN 标志位为 0,ACK 标志位为 1,并且把服务器发来 ACK 的序号字段 +1,放在确定字段中发送给对方,并且在数据段放写 ISN 的 +1

发送完毕后,客户端进入 ESTABLISHED 状态,当服务器端接收到这个包时,也进入 ESTABLISHED 状态,TCP 握手结束。

  • 第一次挥手(FIN=1,seq=x)

假设客户端想要关闭连接,客户端发送一个 FIN 标志位置为1的包,表示自己已经没有数据可以发送了,但是仍然可以接受数据。

发送完毕后,客户端进入 FIN_WAIT_1 状态。

  • 第二次挥手(ACK=1,ACKnum=x+1)

服务器端确认客户端的 FIN 包,发送一个确认包,表明自己接受到了客户端关闭连接的请求,但还没有准备好关闭连接。

发送完毕后,服务器端进入 CLOSE_WAIT 状态,客户端接收到这个确认包之后,进入 FIN_WAIT_2 状态,等待服务器端关闭连接。

  • 第三次挥手(FIN=1,seq=y)

服务器端准备好关闭连接时,向客户端发送结束连接请求,FIN 置为1。

发送完毕后,服务器端进入 LAST_ACK 状态,等待来自客户端的最后一个ACK。

  • 第四次挥手(ACK=1,ACKnum=y+1)

客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 TIME_WAIT状态,等待可能出现的要求重传的 ACK 包。

服务器端接收到这个确认包之后,关闭连接,进入 CLOSED 状态。

客户端等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED 状态。

安全相关

在 Android 开发中,确保文件没有被篡改是非常重要的,尤其是在处理敏感数据时。以下是一些常用的方法和技术,帮助你检测和防止文件被篡改。

  1. 使用数字签名 数字签名是一种常见的方法,用于验证文件的完整性和来源。你可以使用数字签名来确保文件在传输和存储过程中没有被篡改。
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

public class FileSignatureGenerator {
    public static String generateFileSignature(File file) throws NoSuchAlgorithmException, IOException {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        try (FileInputStream fis = new FileInputStream(file)) {
            byte[] buffer = new byte[8192];
            int count;
            while ((count = fis.read(buffer)) > 0) {
                digest.update(buffer, 0, count);
            }
        }
        byte[] hash = digest.digest();
        return Base64.getEncoder().encodeToString(hash);
    }
}
  1. 验证数字签名
public class FileSignatureVerifier {
    public static boolean verifyFileSignature(File file, String expectedSignature) throws NoSuchAlgorithmException, IOException {
        String actualSignature = FileSignatureGenerator.generateFileSignature(file);
        return expectedSignature.equals(actualSignature);
    }
}

Android部分

1. 启动流程

Zygote 进程与 System Server 之间的通信主要通过 Socket 实现。以下是详细的通信过程: Zygote 初始化 Zygote 进程启动后,会创建一个 ServerSocket,监听一个特定的端口(通常是 1123)。 Zygote 进程进入一个循环,等待来自 System Server 的连接请求。 System Server 请求 当 System Server 需要启动一个新的应用进程时,它会通过 Socket 连接到 Zygote 进程。 System Server 会发送一个包含应用启动信息的命令,如应用的包名、类名等。 Zygote 处理请求 Zygote 进程接收到请求后,会解析命令中的信息。 Zygote 会 fork 一个新进程,并将应用启动信息传递给新进程。 新进程会继续加载应用的代码和资源,并启动应用的主 Activity。 新进程启动 新进程启动后,会注册到 ActivityManagerService,完成应用的启动过程。

2. 绘制流程

ViewRoot 对应于 ViewRootImpl 类,它是连接 WindowManager 和 DecorView 的纽带,View 的三大流程均是通过 ViewRoot 来完成的。在 ActivityThread 中,当 Activity 对象被创建完毕后,会将 DecorView 添加到 Window 中,同时会创建 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 建立关联

View 的整个绘制流程可以分为以下三个阶段:

  • measure: 判断是否需要重新计算 View 的大小,需要的话则计算
  • layout: 判断是否需要重新计算 View 的位置,需要的话则计算
  • draw: 判断是否需要重新绘制 View,需要的话则重绘制
    1. 如果想要获取宽高,则必须调用View.post的方法

draw 绘制基本上可以分为六个步骤:

  • 首先绘制View的背景;
  • 如果需要的话,保持canvas的图层,为fading做准备;
  • 然后,绘制View的内容;
  • 接着,绘制View的子View;
  • 如果需要的话,绘制View的fading边缘并恢复图层;
  • 最后,绘制View的装饰(例如滚动条等等)。

RelativeLayout 下面一个View, 这个绘制流程是什么? 测量: RelativeLayout 接收到 MeasureSpec,开始测量。 RelativeLayout 测量 TextView,根据 wrap_content 计算 TextView 的尺寸。 RelativeLayout 根据 TextView 的尺寸和自身的 MeasureSpec 计算自己的尺寸。 布局: RelativeLayout 设置 TextView 的位置,使其居中显示。 RelativeLayout 自己的位置已经确定。

3. Handle

隐式引用:Handler 会隐式地持有对外部类的引用。 延迟消息:发送延迟消息时,Handler 会将消息添加到 Looper 的消息队列中。 内存泄漏:如果 Activity 或 Fragment 已经销毁,但消息队列中仍有未处理的消息,Handler 会继续持有对外部类的引用,导致内存泄漏。 解决方案:在 onDestroy 方法中调用 handler.removeCallbacksAndMessages(null),移除所有待处理的消息,确保 Handler 不再持有对外部类的引用

隐式引用:非静态内部类会隐式地持有对外部类的引用,这是通过编译器生成的合成字段实现的。


public final class MyActivity extends AppCompatActivity {
    private MyHandler handler;

    public final class MyHandler extends Handler {
        private final MyActivity this$0; // 合成字段,持有对外部类的引用

        public MyHandler(Looper looper) {
            super(looper);
            this.this$0 = MyActivity.this; // 初始化合成字段
        }

        @Override
        public void handleMessage(Message msg) {
            // 处理消息
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        this.handler = new MyHandler(Looper.getMainLooper());

        this.handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                // 延迟任务
            }
        }, 10000);
    }
}

延时消息处理流程: 延时消息的添加: MessageQueue 的 next() 方法首先检查是否有消息可用。 如果当前时间小于延时消息的 when 时间,next() 方法会计算出还需要等待的时间,并将这个时间传递给 nativePollOnce 方法。 nativePollOnce 方法的阻塞: nativePollOnce(ptr, nextPollTimeoutMillis) 方法会阻塞当前线程,等待新的消息或事件。 nextPollTimeoutMillis 表示等待的时间,如果为 -1,表示无限期等待;如果为正数,表示等待指定的时间。 延时消息的唤醒: 当延时消息的预定时间到达时,MessageQueue 会通过 nativeWake 方法唤醒 Looper。 nativeWake 方法会通知 nativePollOnce 方法,使其返回,从而解除阻塞状态。 处理延时消息: next() 方法继续执行,从 MessageQueue 中取出延时消息并返回给 Looper。 Looper 调用消息的 target 回调方法(通常是 Handler 的 handleMessage 方法)来处理延时消息。

消息屏障:

do { prevMsg = msg; msg = msg.next; } while (msg != null && !msg.isAsynchronous());

遇到屏障消息: 当 MessageQueue 从队列中取出一个消息时,如果该消息的 target 为 null,则认为这是一个屏障消息。 跳过屏障消息及其之前的消息: 从当前屏障消息开始,逐个检查后续消息,直到找到第一个非屏障消息或队列结束。 如果遇到异步消息(即 isAsynchronous() 返回 true),则停止跳过,因为异步消息具有更高的优先级,可以跳过屏障消息直接处理。 处理非屏障消息: 一旦找到第一个非屏障消息,next() 方法返回该消息,Looper 继续处理该消息。

假设消息队列中有以下消息: 普通消息 A 普通消息 B 屏障消息 C 普通消息 D 异步消息 E 普通消息 F 处理过程如下: Looper 从队列中取出消息 A 并处理。 Looper 从队列中取出消息 B 并处理。 Looper 从队列中取出屏障消息 C。 MessageQueue 的 next() 方法进入 do-while 循环,跳过屏障消息 C 及其之前的消息 B。 do-while 循环继续,跳过普通消息 D。 do-while 循环遇到异步消息 E,停止跳过。 next() 方法返回异步消息 E,Looper 处理消息 E。 Looper 从队列中取出普通消息 F 并处理。 通过这种方式,MessageQueue 确保了屏障消息之前的所有消息都被处理完毕后,再处理屏障消息之后的消息。

IdleHandler 在 Android 开发中,IdleHandler 是一个用于在消息队列空闲时执行任务的机制。IdleHandler 可以在主线程的消息队列没有待处理消息时被调用,这在某些情况下非常有用,例如进行一些清理工作或延迟执行某些任务。 线程安全:queueIdle 方法在主线程中调用,因此在这个方法中执行的操作应该是线程安全的。 性能考虑:避免在 queueIdle 方法中执行耗时操作,因为这会影响应用的响应速度。 多次调用:如果 queueIdle 方法返回 true,则会在每次消息队列空闲时调用该方法。如果只需要调用一次,可以在方法内部移除 IdleHandler。 通过使用 Looper.IdleHandler,你可以在消息队列空闲时执行一些必要的任务,从而优化应用的性能和用户体验。

4. 事件分发

事件分发过程由三个方法共同完成:

dispatchTouchEvent:方法返回值为true表示事件被当前视图消费掉;返回为super.dispatchTouchEvent表示继续分发该事件,返回为false表示交给父类的onTouchEvent处理。

onInterceptTouchEvent:方法返回值为true表示拦截这个事件并交由自身的onTouchEvent方法进行消费;返回false表示不拦截,需要继续传递给子视图。如果return super.onInterceptTouchEvent(ev), 事件拦截分两种情况:  

  • 1.如果该View存在子View且点击到了该子View, 则不拦截, 继续分发 给子View 处理, 此时相当于return false。
  • 2.如果该View没有子View或者有子View但是没有点击中子View(此时ViewGroup 相当于普通View), 则交由该View的onTouchEvent响应,此时相当于return true。

注意:一般的LinearLayout、 RelativeLayout、FrameLayout等ViewGroup默认不拦截, 而 ScrollView、ListView等ViewGroup则可能拦截,得看具体情况。

onTouchEvent:方法返回值为true表示当前视图可以处理对应的事件;返回值为false表示当前视图不处理这个事件,它会被传递给父视图的onTouchEvent方法进行处理。如果return super.onTouchEvent(ev),事件处理分为两种情况:

  • 1.如果该View是clickable或者longclickable的,则会返回true, 表示消费 了该事件, 与返回true一样;
  • 2.如果该View不是clickable或者longclickable的,则会返回false, 表示不 消费该事件,将会向上传递,与返回false一样。

注意:在Android系统中,拥有事件传递处理能力的类有以下三种:

  • Activity:拥有分发和消费两个方法。
  • ViewGroup:拥有分发、拦截和消费三个方法。
  • View:拥有分发、消费两个方法。

5. 三级缓存

LruCache(Least Recently Used Cache)是一种缓存策略,用于存储最近最常使用的数据项。当缓存达到最大容量时,会移除最近最少使用的数据项。LruCache 在 Android 中主要用于内存缓存,特别是在图像缓存中非常常见。

  1. 基本原理 数据结构:LruCache 使用一个哈希表(HashMap)来存储数据项,并使用一个双向链表来维护数据项的访问顺序。 容量限制:LruCache 有一个最大容量,当缓存超过这个容量时,会自动移除最近最少使用的数据项。 访问顺序:每次访问一个数据项时,该数据项会被移到双向链表的头部;当需要移除数据项时,从链表尾部移除。

###Binder 二、Binder 是什么?

  1. 核心概念 Binder 是 Android 独有的跨进程通信机制,基于 Binder 驱动 实现,采用 Client-Server 架构。其核心组件包括: Binder 驱动:内核层组件,负责进程间数据传输和对象管理。 ServiceManager:系统服务注册与查询中心(如 AMS、WMS)。 IBinder 接口:所有 Binder 对象的基类,定义跨进程通信接口。 Parcel:轻量级序列化容器,用于打包传输数据。

  2. 通信流程示例 以调用远程 Service 方法为例: Client 进程 → BinderProxy(代理) → Binder 驱动 → Service 进程 → Binder 实体 → 执行方法 → 返回结果

  3. Binder 的优势 性能高效:仅需一次内存拷贝(传统 IPC 如 Socket 需两次)。 安全性:通过 UID/PID 验证权限,防止非法访问。 面向对象设计:支持跨进程调用对象方法(如 AIDL 接口)。 自动线程管理:Binder 驱动自动分配线程池处理请求。

三、为什么 Android 使用 Binder?

  1. 性能需求 Android 设备资源受限,Binder 的 单次内存拷贝机制 比传统 IPC(如 Socket)更高效。例如: Socket:数据需从用户空间拷贝到内核空间,再拷贝到目标进程。 Binder:数据直接从发送方拷贝到接收方,减少中间步骤。
  2. 安全性需求 Android 系统需要严格的权限控制,Binder 通过以下机制保障安全: UID/PID 验证:系统服务可校验调用者身份。 权限声明:通过 android:permission 在 Manifest 中限制访问。
  3. 组件通信模型适配 Android 应用由 松耦合的组件(Activity、Service、BroadcastReceiver) 构成,Binder 的 C/S 架构天然适配: Service 绑定:通过 bindService() 获取 IBinder 接口。 AIDL 接口:定义跨进程调用方法,自动生成桩代码。 系统服务交互:如 ActivityManagerService(AMS)管理组件生命周期。
  4. 开发效率 Binder 提供高级封装(如 Messenger 和 AIDL),简化开发: AIDL 示例:

六、典型应用场景 四大组件通信: Activity 与 Service 绑定(通过 onBind() 返回 IBinder)。 ContentProvider 跨进程访问数据。 系统服务调用: 通过 ServiceManager.getService() 获取系统服务(如 ActivityManagerNative)。 跨进程数据共享: 使用 AIDL 定义接口,实现进程间方法调用。 消息传递: Messenger 基于 Binder 实现进程间消息队列(适合串行处理请求)。

Binder 是 Android 为满足高性能、安全性和组件化需求而设计的核心 IPC 机制。相比传统 IPC 方式,它在 性能、易用性、安全性 上具有显著优势,成为 Android 系统服务和应用组件通信的基础。开发者应根据场景选择合适的 IPC 方式: 简单数据交换:使用 Bundle 和 ContentProvider。 高性能大数据共享:结合 共享内存 与 Binder 同步控制。 复杂远程调用:通过 AIDL 或 Messenger 实现。

AMMessage PMMessage

6. 三方库原理

  1. Glide 高效的图片加载库,通过缓存、异步加载、生命周期管理和图片转换等机制,提供高性能的图片加载和显示功能。 通过理解和使用这些机制,可以显著提升 Android 应用的性能和用户体验。

请求构建: 用户通过 Glide.with(context).load(url).into(imageView) 构建一个图片加载请求。 Glide 会创建一个 RequestBuilder 对象,用于配置加载选项。 请求调度: RequestBuilder 将请求提交给 RequestManager。 RequestManager 将请求放入队列中,等待执行。 缓存检查: Glide 会先检查内存缓存(LruCache)和磁盘缓存,如果找到缓存的图片,直接返回。 如果没有找到缓存的图片,进入下一步。 图片下载: Glide 使用 OkHttp 或 Volley 等网络库下载图片。 下载完成后,将图片保存到内存缓存和磁盘缓存中。 图片解码: Glide 使用 BitmapFactory 解码图片,根据 ImageView 的尺寸进行缩放,以节省内存。 解码后的图片会被放入内存缓存中。 图片显示: Glide 将解码后的图片显示在 ImageView 中。

使用 Glide 加载大图片时,虽然 Glide 本身有很好的内存管理和缓存机制,但在某些情况下仍然可能会遇到 OOM(Out of Memory)错误。为了避免这种情况,可以采取以下措施:

  1. 设置合理的请求大小 确保 override 方法中的目标大小是合理的,不要设置得过大。例如,如果你的 ImageView 实际上只需要 1000x1000 像素,那么设置 override(1000, 1000) 是合适的。

  2. 使用 thumbnail 方法 Glide 提供了 thumbnail 方法,可以在加载大图之前先加载一个小图,这样可以减少内存压力

  3. 使用 diskCacheStrategy 设置磁盘缓存策略,可以减少对内存的依赖。

  4. 使用 fitCenter 或 centerCrop 这些方法可以帮助调整图片的大小,使其更适合 ImageView 的尺寸。

  5. 使用 skipMemoryCache 如果图片非常大,可以考虑跳过内存缓存,直接从磁盘缓存加载。

  6. OKHttp OKHttp 是一个功能强大、易于使用的 HTTP 客户端库,通过连接池、拦截器、响应缓存、重试机制等特性,提供了高效的网络请求和响应处理能力。理解这些原理和特性,可以帮助开发者更好地使用 OKHttp,优化应用的网络性能和用户体验。

  7. 创建 OkHttpClient 实例

  8. 使用连接池

  9. 添加拦截器 [应用拦截器1] → [应用拦截器2] → ... → [RetryAndFollowUpInterceptor](重试与重定向) → [BridgeInterceptor](补充请求头,如 Content-Type、Host) → [CacheInterceptor](读取/写入缓存) → [ConnectInterceptor](建立连接,TCP + TLS) → [网络拦截器1] → [网络拦截器2] → ... → [CallServerInterceptor](发送请求到服务器,读取响应)

  10. 响应缓存

  11. 重试机制

  12. 发送 GET 请求

支持的协议

HTTP/1.1:默认协议,支持连接复用(Keep-Alive)。 HTTP/2:支持多路复用(Multiplexing),减少 TCP 连接数,提升性能。 WebSocket:长连接通信,适用于实时交互场景。 QUIC(实验性):基于 UDP 的低延迟协议(需额外配置)。 DNS over HTTPS(DoH):通过 Dns 接口支持加密 DNS 查询(如 dnsjava 集成)。

三、请求分发机制(Dispatcher)

  1. 同步与异步请求 同步请求:直接调用 call.execute(),阻塞当前线程。 异步请求:调用 call.enqueue(),由 Dispatcher 的线程池异步执行。
  2. 线程池管理 Dispatcher 默认使用一个共享的线程池(ExecutorService),可自定义: val dispatcher = Dispatcher().apply { maxRequests = 64 // 最大并发请求数 maxRequestsPerHost = 5 // 每个主机的最大请求数 } val client = OkHttpClient.Builder() .dispatcher(dispatcher) .build()

3.准备队列(readyAsyncCalls):等待执行的异步请求。 运行队列(runningAsyncCalls):正在执行的异步请求。 当线程池有空闲线程且未超过 maxRequests 时,从准备队列取出请求执行。

四、缓存机制(Cache)

  1. 缓存策略 OKHttp 基于 HTTP 缓存语义(RFC 7234)实现,支持以下策略: 强制缓存:根据 Cache-Control 头判断是否使用缓存。 协商缓存:使用 ETag 或 Last-Modified 与服务端验证缓存有效性。
  2. 缓存存储 DiskLruCache:基于文件系统的 LRU 缓存(OKHttp 内置实现)。 内存缓存:可通过 Cache-Control: max-age=0 强制跳过内存缓存。 缓存键:使用请求 URL 和请求头生成唯一键。
  3. 缓存拦截器(CacheInterceptor) 缓存拦截器的核心逻辑: 读取缓存:根据请求查找匹配的缓存响应。 验证缓存有效性: 若缓存未过期,直接返回缓存响应。 若缓存过期,发送 If-None-Match 或 If-Modified-Since 验证。 写入缓存:若响应包含 Cache-Control 头,且响应体可缓存,则写入磁盘。
  4. 缓存配置示例

val cache = Cache(File(context.cacheDir, "http-cache"), 10 * 1024 * 1024) // 10MB val client = OkHttpClient.Builder() .cache(cache) .build()

五、连接管理与连接池

  1. 连接复用(Connection Pooling) 复用条件:相同 Host、Port、协议(HTTP/1.1 或 HTTP/2)。 默认策略:最多保存 5 个空闲连接,超时时间为 1 分钟。 连接池操作: val connectionPool = ConnectionPool(5, 1, TimeUnit.MINUTES) val client = OkHttpClient.Builder() .connectionPool(connectionPool) .build()

  2. 连接建立流程 DNS 解析:通过 Dns 接口获取 IP 地址(默认使用系统 DNS)。 TCP 握手:建立 TCP 连接。 TLS 握手:协商加密协议(如 TLS 1.2/1.3)。 协议协商:通过 ALPN 确定 HTTP 版本。

七、性能优化建议 启用 HTTP/2:减少连接数,提升并发性能。 合理配置缓存:避免重复请求,降低流量消耗。 控制并发数:避免线程池资源耗尽。 复用连接:减少 TCP 握手延迟。 使用 GZIP 压缩:通过 Accept-Encoding: gzip 减少传输体积。

八、总结 OKHttp 通过 模块化设计 和 高效的底层实现,成为 Android 开发中最受欢迎的 HTTP 客户端。其核心优势包括: 灵活的拦截器链:支持自定义请求/响应处理逻辑。 智能的连接管理:复用连接、自动重试、协议协商。 完善的缓存机制:基于 HTTP 标准的缓存策略。 高性能异步分发:线程池调度与并发控制。 通过合理配置和优化,OKHttp 能显著提升应用的网络性能和用户体验。

腾讯直播

  1. RTMP (Real-Time Messaging Protocol) 特点:RTMP 是一种低延迟的流媒体协议,广泛用于直播推流和播放。 用途:适用于实时直播,如游戏直播、体育赛事等。 格式:rtmp://your-stream-url
  2. HLS (HTTP Live Streaming) 特点:HLS 是一种基于 HTTP 的流媒体传输协议,支持自适应比特率(ABR),适合不同网络条件下的播放。 用途:适用于需要高兼容性和自适应比特率的直播场景。 格式:https://your-stream-url.m3u8

记录卡顿信息和首帧加载时间:通过 TXLivePlayer 的回调方法 onPlayEvent 和 onNetStatus 来记录卡顿信息和首帧加载时间。 处理卡顿或网络不好的情况:通过重新连接、降低画质和显示提示信息来处理卡顿或网络不好的情况。 记录日志:将关键信息记录到日志中,方便后续分析和调试。

    @Override
    public void onPlayEvent(int event, Bundle param) {
        switch (event) {
            case TXLiveConstants.PLAY_EVT_PLAY_BEGIN:
                // 首帧加载完成
                long firstFrameTime = System.currentTimeMillis() - startTime;
                Log.d("LivePlayActivity", "First frame loaded in " + firstFrameTime + " ms");
                break;
            case TXLiveConstants.PLAY_ERR_NET_DISCONNECT:
                // 网络断开
                Log.e("LivePlayActivity", "Network disconnected");
                handleNetworkError();
                break;
            case TXLiveConstants.PLAY_WARNING_VIDEO_PLAY_STUTTER:
                // 卡顿警告
                Log.w("LivePlayActivity", "Video stutter detected");
                handleStutter();
                break;
            default:
                break;
        }
    }
    
     @Override
    public void onNetStatus(Bundle status) {
        // 网络状态变化
        int currentBufferDuration = status.getInt(TXLiveConstants.NET_STATUS_CURRENT_BUFFER_DURATION, 0);
        int totalBufferDuration = status.getInt(TXLiveConstants.NET_STATUS_TOTAL_BUFFER_DURATION, 0);
        Log.d("LivePlayActivity", "Buffer duration: " + currentBufferDuration + "/" + totalBufferDuration);
    }

    private void handleNetworkError() {
        // 处理网络断开的情况
        Toast.makeText(this, "网络断开,请检查网络连接", Toast.LENGTH_SHORT).show();
        // 可以尝试重新连接
        mLivePlayer.startPlay("https://your-live-url", TXLivePlayer.PLAY_TYPE_LIVE_RTMP);
    }

    private void handleStutter() {
        // 处理卡顿的情况
        Toast.makeText(this, "视频卡顿,请稍后再试", Toast.LENGTH_SHORT).show();
        // 可以尝试降低画质
        mLivePlayer.setVideoDecoderParam(TXLivePlayer.VIDEO_PARAM_LOW_LATENCY, true);
    }

8. 优化

内存泄漏优化

静态变量持有 Activity 上下文

Handler 泄漏

BroadcastReceiver 泄漏

未注销的监听器

未关闭的资源

匿名内部类和 Lambda 表达式

Bitmap 和 Drawable 泄漏

虚拟机栈中的局部变量。 方法区的类静态属性。 方法区的常量引用。 JNI(Native)引用。

GC Root 是垃圾回收器进行可达性分析的起点,以下类型的对象可以作为 GC Root: 类型 描述 Thread 正在运行的线程 活动中的 Activity 或 Context 实 当前界面上下文 ClassLoader 类加载器实例 static 变量 静态字段所引用的对象 JNI(Native)引用 通过 Native 代码创建的对象引用 正在运行的 Runnable 或 FutureTask 线程任务 正在等待通知或被阻塞的线程 如调用了 wait() 的线程

LeakCanary

检测内存泄漏:LeakCanary 会定期检查应用的内存使用情况,特别是当 Activity 被销毁后,检查这些 Activity 是否仍然被引用。 捕获堆栈信息:一旦检测到潜在的内存泄漏,LeakCanary 会捕获当前的堆栈信息。 分析堆栈信息:LeakCanary 使用第三方库(如 Google 的 HPROF 工具)来分析捕获的堆栈信息,确定哪些对象导致了内存泄漏。

  1. RefWatcher 是 LeakCanary 的核心组件,负责监控对象的生命周期。它通过弱引用(WeakReference)来跟踪对象,当对象被垃圾回收时,RefWatcher 会收到通知。

2 HeapDump 当 RefWatcher 检测到潜在的内存泄漏时,它会触发一个堆转储(Heap Dump)。堆转储是一个包含应用当前内存状态的文件,LeakCanary 会分析这个文件来确定泄漏的原因。

3 Analysis LeakCanary 使用 Google 的 HPROF 工具来分析堆转储文件。HPROF 是一个 Java 虚拟机提供的工具,可以生成和解析堆转储文件。

4 Displaying Results LeakCanary 会生成详细的报告,并通过通知或日志的方式展示给开发者。报告中包含了泄漏对象的路径和相关的堆栈信息

RecycleView优化

  1. 使用 ViewHolder 模式 ViewHolder 模式是 RecyclerView 的核心设计之一,通过复用视图来减少 findViewById 的调用次数,提高性能。

  2. 减少布局复杂度 复杂的布局会增加 RecyclerView 的渲染时间。尽量使用简单的布局,并避免嵌套过多的视图层次。

  3. 使用 DiffUtil 进行数据更新 DiffUtil 可以高效地计算新旧数据集之间的差异,并只更新发生变化的部分,而不是重新加载整个列表。

  4. 使用 ItemAnimator 控制动画 ItemAnimator 可以控制 RecyclerView 中项的动画效果,合理使用可以提升用户

  5. 优化图片加载 使用高效的图片加载库(如 Glide 或 Picasso)来异步加载图片,并进行缓存,避免阻塞主线程。

  6. 使用 setHasFixedSize 如果 RecyclerView 的项高度固定,可以调用 setHasFixedSize(true),这样 RecyclerView 可以更高效地处理布局变化。

  7. 限制 LayoutManager 的预加载项 通过设置 LayoutManager 的预加载项数量,可以减少不必要的视图创建。

  8. 使用 RecyclerView.RecycledViewPool RecycledViewPool 可以在多个 RecyclerView 之间共享复用的视图,提高性能。

  9. 避免在 onBindViewHolder 中进行耗时操作 onBindViewHolder 方法会在主线程中调用,因此应避免在此方法中进行耗时操作,如网络请求或复杂的计算。可以将这些操作移到后台线程中。

  10. 使用 notifyItemChanged 精确更新 当只需要更新某个特定项时,使用 notifyItemChanged 方法,而不是 notifyDataSetChanged,以减少不必要的刷新。

启动优化

1. 延迟初始化

将非必要的初始化操作推迟到首次使用时进行,避免在应用启动时执行过多任务。 使用 Lazy 或 ViewModel 来实现懒加载。

2. 减少主线程工作

避免在主线程上执行耗时操作,如网络请求、数据库查询等。 使用异步任务(如 AsyncTask、HandlerThread 或 WorkManager)将耗时操作移到后台线程。

3. 优化布局文件

简化布局层级,减少嵌套视图的数量。 使用 ConstraintLayout 来替代复杂的嵌套布局,提高布局性能。

4.预加载资源

在应用启动前或启动过程中预加载一些静态资源(如图片、字体等),以减少首次渲染的时间。 使用 SplashScreen API 提供平滑的启动体验。

5.减少依赖库

审查项目中的依赖库,移除不必要的库以减小 APK 大小。 使用 ProGuard 或 R8 进行代码混淆和压缩,减少最终包的体积

image.png

image.png

image.png

image.png

Android UI 自动化测试框架

Espresso 简介:Google 推出的另一个自动化测试框架,专注于单个应用内的UI测试。 特点: 简洁的API设计,易于上手。 高效的同步机制,确保测试的稳定性和可靠性。 使用场景:适用于单个应用内部的UI测试,尤其是复杂的交互流程。

优势

简洁的 API Espresso 提供了一套简洁且直观的 API,使得编写测试代码变得更加容易。 例如,onView(withId(R.id.button)).perform(click()) 一行代码即可完成点击按钮的操作。 高效的同步机制

Espresso 内置了同步机制,能够自动等待 UI 线程空闲后再执行下一步操作,避免了因异步操作导致的测试失败。 这使得测试更加稳定和可靠。

强大的匹配器 Espresso 提供了丰富的匹配器(Matcher),可以方便地定位和操作 UI 元素。 例如,withId、withText、withContentDescription 等匹配器。

集成测试 Espresso 可以与 JUnit 结合使用,提供完整的单元测试和集成测试解决方案。 通过 ActivityTestRule,可以轻松启动和管理被测 Activity。

社区支持 Espresso 拥有活跃的社区和丰富的文档资源,遇到问题时可以很容易找到解决方案。 官方支持 作为 Google 官方推荐的测试框架,Espresso 得到了持续的更新和支持,确保与最新版本的 Android 兼容。

示例代码解释 @Rule: ActivityTestRule 用于启动和管理被测 Activity。

onView: 用于定位 UI 元素。

withId: 匹配指定 ID 的视图。

typeText: 在输入框中输入文本。

click: 点击按钮。

matches: 验证视图的状态。

withText: 匹配指定文本的视图。

Flutter部分

1. Widget渲染

Widget并不真正的渲染对象 。是的,事实上在 Flutter 中渲染是经历了从 Widget 到  Element 再到 RenderObject 的过程。

Widget 和 Element 的基本概念 Widget: Widget 是不可变的配置文件,描述了 UI 的结构和样式。 Widget 是不可变的,一旦创建就不能修改。 Element: Element 是 Widget 在渲染树中的具体实例,是可变的。 Element 代表了 Widget 在渲染树中的位置和状态。

在这个示例中: 每个 CustomText widget 对应一个 Element: 每个 CustomText widget 都会在渲染树中生成一个独立的 Element。 因此,一个 CustomText widget 类型对应了四个 Element 实例。 总结 一对多关系:一个 Widget 类型可以对应多个 Element 实例。 即使 Widget 类型相同,每个实例仍然对应一个独立的 Element。 每个 Widget 实例在渲染树中都有一个对应的 Element。 通过这些解释,我们可以更清晰地理解 Flutter 中 Widget 和 Element 之间的关系。具体来说: 每个 Widget 实例对应一个 Element:即使是相同的 Widget 类型,每个实例仍然对应一个独立的 Element。 一对多关系:一个 Widget 类型可以对应多个 Element 实例。

2. 如何刷新

调用 setState 方法: 当需要更新状态时,调用 setState 方法。 setState 方法会标记当前 State 对象为“dirty”,并安排一次框架重绘。 标记为“dirty”: setState 方法内部会调用 _element.markNeedsBuild() 方法,将当前 State 对象的 Element 标记为需要重建。 框架调度重绘: Flutter 框架会在合适的时机调用 State 对象的 build 方法,重新构建 UI。 构建新的 Element 树: build 方法返回一个新的 Widget 树,框架会根据新的 Widget 树构建新的 Element 树。 比较 Element 树: 框架会比较新的 Element 树和旧的 Element 树,确定哪些部分需要更新。 重绘: 只有发生变化的部分会被重绘,未变化的部分不会被重新绘制,从而实现局部刷新。

setState 方法:标记当前 State 为“dirty”,并安排一次框架重绘。 markNeedsBuild 方法:将 Element 标记为需要重建。 scheduleBuildFor 方法:将需要重建的 Element 添加到待处理列表中,并调度一个帧回调。 _handleBeginFrame 方法:处理帧回调,调用 _flushDirtyElements 方法。 _flushDirtyElements 方法:遍历所有标记为“dirty” 的 Element,调用 rebuild 方法。 rebuild 方法:重新构建 Element,更新 Widget 和 RenderObject。

Bloc 刷新

Equatable:简化了对象之间的相等性检查,避免了手动实现 == 和 hashCode 方法的繁琐过程。 状态类:继承 Equatable 并实现 props 属性,确保状态对象的正确比较。 事件类:通常也继承 Equatable,以便在事件之间进行正确的比较。 Bloc:使用 Bloc 管理状态变化,并在状态变化时触发 UI 重建。 通过使用 Equatable,你可以确保状态对象的正确比较,从而提高应用的性能和可维护性。

在 Flutter 中,BlocBuilder 和 BlocSelector 的局部刷新机制依赖于 Flutter 的小部件树和状态管理机制。为了理解 BlocBuilder 和 BlocSelector 如何判断是否需要重新构建小部件,我们需要了解以下几个关键概念:

  1. 小部件树和状态管理 在 Flutter 中,每个小部件都有一个对应的 Element 对象,Element 对象负责将小部件渲染到屏幕上。当状态发生变化时,Flutter 会重新构建受影响的小部件,并比较新旧 Element 树,以确定哪些部分需要更新。
  2. BlocBuilder 和 BlocSelector 的工作原理 BlocBuilder BlocBuilder 是一个高阶小部件,它监听 BLoC 的状态变化,并在状态变化时重新构建其子小部件。BlocBuilder 的核心逻辑如下: 监听状态变化:BlocBuilder 订阅 BLoC 的状态流,当状态发生变化时,会触发 builder 回调。 重新构建子小部件:在 builder 回调中,根据新的状态构建新的子小部件。 比较新旧小部件:Flutter 会比较新旧 Element 树,确定哪些部分需要更新。 BlocSelector BlocSelector 是 BlocBuilder 的一个变种,它允许你选择性地监听状态的一部分。BlocSelector 的核心逻辑如下: 选择状态的一部分:通过 selector 函数,从完整状态中提取出需要关注的部分。 监听选择后的状态:BlocSelector 订阅选择后的状态流,当选择后的状态发生变化时,触发 builder 回调。 重新构建子小部件:在 builder 回调中,根据新的选择后的状态构建新的子小部件。 比较新旧小部件:Flutter 会比较新旧 Element 树,确定哪些部分需要更新。
  3. 判断是否需要重新构建 Flutter 在决定是否需要重新构建小部件时,会进行以下步骤: Key 和运行类型:Flutter 会检查小部件的 key 和运行类型(runtimeType)。如果 key 和 runtimeType 都相同,则认为是同一个小部件。 状态比较:对于 BlocBuilder 和 BlocSelector,Flutter 会比较新旧状态。如果状态发生变化,且 builder 回调返回的新小部件与旧小部件不同,则会重新构建小部件。

3. 如何实现异步

juejin.cn/post/738328… Future Future 是 Dart 中用于表示异步操作结果的对象。 Future 可以通过 async 关键字和 await 表达式来使用。 Isolate Isolate 是 Dart 中的一个概念,用于实现多线程或多进程。 在 Flutter 中,默认只有一个主线程(UI 线程),但是可以通过 Isolate 来实现并发处理。

4.如何和原生通信的

flutter定义了三种不同类型的Channel

  • BasicMessageChannel:用于传递字符串和半结构化的信息。
  • MethodChannel:用于传递方法调用(method invocation)。
  • EventChannel: 用于数据流(event streams)的通信。

5.Stream:

异步数据流,用于处理一系列异步事件。 关键方法:包括 StreamController 的 sink 和 stream,以及 Stream 的监听和转换方法。

是的,Flutter 的 UI 架构在很多方面与 Jetpack Compose 相似,它们都采用了声明式编程模型,并且在处理复杂的布局和性能优化方面有类似的优势。以下是一些关键点,说明 Flutter 的 UI 架构如何处理复杂的布局和避免布局层级过深的问题:

  1. 声明式 UI Flutter 采用声明式编程模型,这意味着你只需要描述 UI 的最终状态,而不是手动管理 UI 的更新过程。这种模型使得 Flutter 可以更高效地管理和优化 UI 的渲染。
  2. 可组合的 Widget Flutter 的 UI 是由多个可组合的 Widget 构建的。每个 Widget 都是一个独立的单元,可以组合成更复杂的 UI 结构。这种模块化的设计使得 Flutter 可以更灵活地处理复杂的布局。
  3. 智能重建 Flutter 有一个智能的重建机制,只有当相关的状态发生变化时,才会重新构建和绘制相应的 Widget。这意味着即使布局层级很深,也只有必要的部分会被重新计算和渲染,从而提高了性能。
  4. 无视图层次结构 传统的 Android View 系统依赖于视图层次结构,每个视图都是一个独立的对象,需要管理其生命周期和状态。这导致了在布局层级较深时,性能和内存开销会显著增加。而 Flutter 没有传统的视图层次结构,所有的 UI 元素都是通过 Widget 来描述的,这减少了内存开销和性能损耗。
  5. 优化的渲染管道 Flutter 的渲染管道经过优化,可以高效地处理复杂的 UI。它使用一种称为“diffing”的技术来最小化不必要的渲染操作。当状态发生变化时,Flutter 会计算出最小的差异,并只更新那些需要更新的部分。
  6. 状态管理 Flutter 提供了多种状态管理方案,如 Provider、Riverpod、Bloc 等,这些方案可以帮助你更好地管理应用的状态,减少嵌套的 Widget 数量,使得布局结构更加扁平,更容易管理和优化。
  7. 动态布局 Flutter 支持动态布局,可以根据不同的条件和状态动态地生成 UI。这种灵活性使得即使在复杂的布局中,也能够高效地处理各种情况。

3. 哪些问题

  1. 重绘和布局开销:过度的重绘和布局可能导致性能瓶颈。
  2. webview 问题
  3. 适配问题
  4. UI问题: ListView 有居上默认高度、ClipRRect 解决Container设置圆角失效问题,TextOverflow.ellipsis不生效,需要父布局进行约束

//todo java 和 Flutter 页面互相压栈有什么问题。

Koltin部分

1.协成

问题?
inline  内联函数是什么?
委托是什么?
livedata 实现原理是什么? 数据倒灌,多次postValue问题

协程 是一种轻量级的并发模型,允许你在不阻塞当前线程的情况下执行异步操作。协程的核心特点是: 非阻塞性:协程可以在不阻塞当前线程的情况下暂停和恢复执行。 挂起函数(Suspend Function):协程中的异步操作通常封装在挂起函数中,这些函数可以在不阻塞当前线程的情况下暂停执行。

GlobalScope:全局作用域,生命周期与应用程序相同。 CoroutineScope:通用作用域,手动管理协程生命周期。 viewModelScope:Android ViewModel 内置作用域,生命周期与 ViewModel 相同。 lifecycleScope:Android Activity 或 Fragment 内置作用域,生命周期与 Activity 或 Fragment 相同。 coroutineScope:挂起函数,等待所有子协程完成。 supervisorScope:挂起函数,独立管理子协程的失败

使用协成完成

fun main() = runBlocking { val job1 = async { fetchUserName() } val job2 = async { fetchUserAge() }

// 等待所有异步任务完成并获取结果
val userName = job1.await()
val userAge = job2.await()

println("User Name: $userName, User Age: $userAge")

}

async 和 await:用于启动异步任务并等待结果。 CoroutineScope:用于管理协程的作用域。 supervisorScope:用于处理异常,确保一个任务失败不会影响其他任务。 awaitAll:用于等待多个任务的结果。

import kotlinx.coroutines.*

suspend fun fetchUserName(): String {
    delay(1000) // 模拟网络请求延迟
    return "Alice"
}

suspend fun fetchUserAge(): Int {
    delay(1500) // 模拟网络请求延迟
    return 30
}

fun main() = runBlocking {
    val results = listOf(
        async { fetchUserName() },
        async { fetchUserAge() }
    ).awaitAll()

    val userName = results[0] as String
    val userAge = results[1] as Int

    println("User Name: $userName, User Age: $userAge")
}

launch 与 async区别

launch 适用于启动不需要返回结果的协程,如后台任务或更新 UI。不提供直接的方法来等待结果(需调用 join())

async 适用于启动需要返回结果的协程,并且可以通过 await() 获取结果。

fun main() = runBlocking { val job1 = launch { delay(500L) println("Task 1 completed") }

val deferred1 = async {
    delay(1000L)
    "Task 2 result"
}

println("Main thread is doing something else...")

job1.join()  // 等待 Task 1 完成
val result = deferred1.await()  // 等待 Task 2 完成并获取结果

println("Final result: $result")

}

// 输出: // Main thread is doing something else... // Task 1 completed // Final result: Task 2 result

2.委托

3.let 和run 区别

run 也是一个作用域函数,它接受一个 lambda 表达式作为参数,并在接收者对象的上下文中执行该 lambda 表达式。与 let 不同的是,run 的返回值是 lambda 表达式的返回值,而不是接收者对象本身。

接收者引用: let:在 lambda 表达式中,接收者对象通过 it 引用。 run:在 lambda 表达式中,接收者对象通过 this 引用,或者可以省略 this。 返回值: let:返回 lambda 表达式的返回值。 run:返回 lambda 表达式的返回值。 适用场景: let:通常用于对可空对象进行安全调用,并在 lambda 表达式中处理非空情况。 run:通常用于在一个对象的上下文中执行一系列操作,并返回最终结果。

let:


name?.let {
    println("Hello, $it")
} // 输出: Hello, Kotlin

run:

val name: String? = "Kotlin"

val result = name?.run {
    val upperCase = this.toUpperCase()
    "Hello, $upperCase"
}

println(result) // 输出: Hello, KOTLIN

apply 定义:apply 是一个作用域函数,它接受一个 lambda 表达式作为参数,并在该对象的上下文中执行这个 lambda 表达式。 返回值:apply 返回对象本身。 接收者:apply 的接收者(即 this)在 lambda 表达式中可以直接通过 this 关键字访问。 适用场景:通常用于初始化对象,或者对对象进行一系列修改但不需要返回新的值。

class User(var name: String, var age: Int)

val user = User("Alice", 30).apply {
    name = "Bob"
    age = 35
}

println(user.name) // 输出: Bob
println(user.age)  // 输出: 35

also: 定义:also 是一个作用域函数,它接受一个 lambda 表达式作为参数,并在该对象的上下文中执行这个 lambda 表达式。 返回值:also 返回对象本身。 接收者:also 的接收者(即 this)在 lambda 表达式中可以通过 it 关键字访问。 适用场景:通常用于副作用操作,例如日志记录或调试,同时返回对象本身。

val list = mutableListOf("a", "b", "c").also {
    println("List size: ${it.size}")
}

list.add("d")

println(list) // 输出: [a, b, c, d]

with: 定义:with 是一个作用域函数,它接受一个对象和一个 lambda 表达式作为参数,并在该对象的上下文中执行这个 lambda 表达式。 返回值:with 返回 lambda 表达式的执行结果。 接收者:with 的接收者(即 this)在 lambda 表达式中可以直接通过 this 关键字访问。 适用场景:通常用于对对象进行一系列操作,并返回最终结果。与 run 类似,但不作为扩展函数调用。

class User(var name: String, var age: Int)

val user = User("Alice", 30)

val result = with(user) {
    name = "Bob"
    age = 35
    "User updated: $name, $age"
}

println(result) // 输出: User updated: Bob, 35

3.JetPack

LiveData:用于观察和处理数据的变化。 ViewModel:用于存储和管理 UI 相关的数据,确保数据在配置更改时仍然可用。 Room:用于处理数据库操作,提供编译时检查的 SQL 查询。 Navigation:用于处理应用内的导航操作。 WorkManager:用于处理后台任务,确保任务在设备重启后仍然可以执行。

livedata

观察者注册 (observe 方法): observe 方法将 Observer 注册到 LiveData 中,并将 Observer 包装成 LifecycleBoundObserver。 LifecycleBoundObserver 实现了 GenericLifecycleObserver,可以监听 LifecycleOwner 的生命周期变化。 数据更新 (setValue 和 postValue 方法): setValue 方法在主线程中直接更新数据,并调用 setInternalValue 方法。 postValue 方法在后台线程中调用 setValue,确保数据更新在主线程中进行。 数据分发 (dispatchingValue 方法): dispatchingValue 方法遍历所有活跃的 Observer,并调用它们的 onChanged 方法。 通过 mVersion 版本号机制,确保每个 Observer 只接收一次更新。 生命周期感知 (LifecycleBoundObserver 类): LifecycleBoundObserver 监听 LifecycleOwner 的生命周期变化,当 LifecycleOwner 处于活跃状态时,Observer 会接收到数据更新。 当 LifecycleOwner 被销毁时,Observer 会被移除。 总结 LiveData 通过 Observer 和 LifecycleOwner 的结合,实现了数据的生命周期感知和自动更新。 关键逻辑 包括观察者的注册、数据的更新和分发、以及生命周期的感知。 线程安全 通过 @Volatile 关键字和同步机制保证,确保在多线程环境下数据的一致性和正确性。 通过这些机制,LiveData 能够在数据变化时自动通知 UI 更新,同时避免了内存泄漏和不必要的数据更新。

setValue 和 postValue 的区别 setValue(T value): 线程限制:必须在主线程上调用。 立即更新:会立即更新 LiveData 的值,并且立即通知所有观察者。 postValue(T value): 线程限制:可以在任何线程上调用。 异步更新:会将值发布到主线程的消息队列中,稍后在主线程上更新 LiveData 的值并通知观察者。 频繁调用的影响 性能问题: 主线程阻塞:如果频繁调用 setValue,可能会导致主线程频繁被阻塞,影响 UI 的响应速度。 消息队列积压:如果频繁调用 postValue,可能会导致主线程的消息队列积压,延迟其他任务的执行。

LiveData频繁刷新问题

在 Android 开发中,使用 LiveData 时如果频繁更新数据(如传感器数据、实时消息推送等),可能会导致 UI 刷新过于频繁,影响性能甚至造成卡顿。下面将从原理、问题定位、优化策略和实际代码示例等方面详细讲解如何优雅地处理 LiveData 的频繁刷新。
  1. 使用 distinctUntilChanged() 过滤重复值

  2. 使用 throttleFirst() / debounce() 控制刷新频率(配合 Kotlin Flow) 由于 LiveData 本身没有内置节流机制,可以借助 Kotlin Flow 实现: (1)使用 Flow + throttleFirst(固定时间间隔只取第一个)

    val throttledLiveData = viewModel.dataFlow .throttleFirst(500, TimeUnit.MILLISECONDS) .asLiveData()

throttledLiveData.observe(viewLifecycleOwner) { updateUI(it) } (2)使用 debounce()(仅响应最后一次操作) 适用于输入框搜索建议、滑动事件等场景: val debouncedLiveData = viewModel.dataFlow .debounce(300) .asLiveData()

debouncedLiveData.observe(viewLifecycleOwner) { updateUI(it) }

  1. 使用 MediatorLiveData 手动控制更新逻辑 你可以通过 MediatorLiveData 封装自己的更新逻辑,例如限制更新频率、过滤无效数据等。 示例:每 200ms 最多更新一次

    class ThrottledLiveData(private val interval: Long = 200) : MediatorLiveData() { private var lastTimestamp = 0L

    fun setValueIfThrottled(value: T) { val now = System.currentTimeMillis() if (now - lastTimestamp > interval) { lastTimestamp = now postValue(value) } } }

    1. 在 ViewModel 中减少不必要的更新

    避免频繁调用 MutableLiveData.postValue(),尤其是在循环或高频率回调中。 优化建议: 缓存当前值,在发生变化后再更新。 合并多个小更新为一个整体更新。 使用 AtomicReference 或 ConcurrentHashMap 来管理状态。 示例: class SensorViewModel : ViewModel() { private val _sensorData = MutableLiveData() val sensorData: LiveData = _sensorData

    private var lastValue: Float = 0f private val threshold = 0.1f

    fun onSensorChanged(newValue: Float) { if (abs(newValue - lastValue) > threshold) { _sensorData.postValue(newValue) lastValue = newValue } } } 三、进阶技巧:结合 WorkManager 或 Handler 延迟更新 对于某些不需要立即刷新 UI 的数据,可以通过延迟执行来合并多次更新。 示例:使用 Handler 延迟更新 private val handler = Handler(Looper.getMainLooper()) private var pendingUpdate: Runnable? = null

fun updateDataWithDelay(data: String) { pendingUpdate?.let { handler.removeCallbacks(it) }

pendingUpdate = Runnable {
    _liveData.value = data
}

handler.postDelayed(pendingUpdate!!, 200)

}

image.png

ViewModel

ViewModel 的主要特点是它具有生命周期感知能力,能够在配置更改(如屏幕旋转)时保留数据,避免数据丢失。了解 ViewModel 的生命周期对于正确使用它非常重要。 ViewModel 的生命周期 ViewModel 的生命周期与 Activity 或 Fragment 的生命周期紧密相关,但它比 Activity 或 Fragment 的生命周期更长。具体来说,ViewModel 的生命周期如下: 创建: 当 Activity 或 Fragment 被创建时,ViewModel 也会被创建。 通常通过 ViewModelProvider 获取 ViewModel 实例。 保留: 在配置更改(如屏幕旋转)时,ViewModel 会被保留,不会被销毁。 这意味着 ViewModel 中的数据在配置更改前后仍然可用。 销毁: 当 Activity 或 Fragment 被永久销毁时(例如,用户导航离开或系统回收资源),ViewModel 也会被销毁。 ViewModel 的 onCleared() 方法会在 ViewModel 被销毁时调用,可以在这里释放资源。 ViewModel 是一个存储和管理 UI 相关数据的类,设计为在配置更改(如屏幕旋转)时存活。它与 Lifecycle 组件配合使用,确保数据的一致性和持久性。

viewModel = ViewModelProvider(this).get(MyViewModel::class.java)

kotlin lazy

懒加载:lazy 函数允许你延迟初始化对象,直到第一次访问时才进行初始化。 线程安全模式:LazyThreadSafetyMode 枚举提供了三种模式来控制线程安全。 实现原理:lazy 函数的实现基于 LazyImpl 类,使用 volatile 变量和 synchronized 关键字来确保线程安全。

lazy 在 Kotlin 中通过代理模式和线程安全机制实现了延迟初始化。它在第一次访问属性时进行初始化,并在后续访问时返回已初始化的值。lazy 提供了三种线程安全模式,可以根据具体需求选择合适的模式。希望这些解释能帮助你更好地理解和使用 lazy。如果你有任何其他问题或需要进一步的帮助,请随时提问!

private class SynchronizedLazyImpl<out T>(
    private val initializer: () -> T,
    lock: Any? = null
) : Lazy<T>, Serializable {
    private var _value: Any? = UNINITIALIZED_VALUE

    @Volatile
    private var _lock: Any? = lock ?: this

    override val value: T
        get() {
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }

            return synchronized(_lock) {
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {
                    @Suppress("UNCHECKED_CAST") _v2 as T
                } else {
                    val typedValue = initializer.invoke()
                    _value = typedValue
                    typedValue
                }
            }
        }

    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

    override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."

    private fun readResolve(): Any = value
}

延迟初始化:_value 初始值为 UNINITIALIZED_VALUE,表示尚未初始化。当第一次访问 value 属性时,会调用 initializer 函数进行初始化。 线程安全:synchronized 关键字确保在多线程环境下只有一个线程能够进行初始化操作。 双重检查锁定:在 get 方法中,先进行一次非同步的检查,如果 _value 已经初始化,则直接返回。否则,进入同步块进行第二次检查,确保线程安全。 序列化支持:readResolve 方法确保在反序列化时返回已初始化的值。

设计模式

单例模式 (Singleton Pattern)

描述: 确保一个类只有一个实例,并提供一个全局访问点。 Android 应用场景: Application 类、数据库帮助类、网络请求管理类等。

工厂模式 (Factory Pattern)

描述: 定义一个创建对象的接口,但让子类决定实例化哪一个类。 Android 应用场景: 创建不同类型的 View、Adapter 等。

抽象工厂模式 (Abstract Factory Pattern)

描述: 提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。 Android 应用场景: 创建不同主题的 UI 组件集合。

建造者模式 (Builder Pattern)

描述: 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。 Android 应用场景: 构建复杂的 AlertDialog、Intent 等。

原型模式 (Prototype Pattern)

描述: 通过复制现有对象来创建新对象,而不是通过常规构造函数。 Android 应用场景: 复制 Parcelable 对象。

适配器模式 (Adapter Pattern)

描述: 将一个类的接口转换成客户希望的另一个接口。 Android 应用场景: RecyclerView.Adapter、SpinnerAdapter 等。

装饰器模式 (Decorator Pattern)

描述: 动态地给一个对象添加一些额外的职责。 Android 应用场景: InputStream 的各种装饰类(如 BufferedInputStream)。

代理模式 (Proxy Pattern)

描述: 为其他对象提供一种代理以控制对这个对象的访问。 Android 应用场景: ContentProvider、网络请求代理等。

观察者模式 (Observer Pattern)

描述: 定义对象之间的一对多依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。 Android 应用场景: LiveData、BroadcastReceiver 等。

策略模式 (Strategy Pattern)

描述: 定义一系列算法,把它们一个个封装起来,并且使它们可以互相替换。 Android 应用场景: 不同的排序算法、不同的数据处理策略等。

命令模式 (Command Pattern)

描述: 将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化。 Android 应用场景: Intent 机制、命令队列等。

状态模式 (State Pattern)

描述: 允许一个对象在其内部状态改变时改变它的行为。 Android 应用场景: 播放器的状态管理(播放、暂停、停止等)。

责任链模式 (Chain of Responsibility Pattern)

描述: 使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。 Android 应用场景: Activity 的生命周期回调、事件分发等。

高内聚和模板化是软件设计中的两种重要模式,它们各自有不同的优势和劣势。下面分别介绍这两种模式,并对比它们的优劣。 高内聚(High Cohesion) 定义 高内聚是指一个模块内部的各个组成部分紧密相关,共同完成一个明确的功能。高内聚的模块通常具有单一职责,每个模块专注于解决一个特定的问题。 优势 易于理解和维护:高内聚的模块功能单一,代码结构清晰,易于理解和维护。 降低耦合度:高内聚的模块通常与其他模块的依赖关系较少,降低了系统的耦合度,提高了系统的可扩展性和可测试性。 提高重用性:高内聚的模块更容易被复用,因为它们通常封装了特定的功能,可以在不同的上下文中使用。 减少错误:由于模块功能单一,代码逻辑简单,减少了出错的可能性。 劣势 过度细分:过度追求高内聚可能导致模块过于细分,增加了模块的数量,管理成本上升。 灵活性受限:高内聚的模块通常功能单一,如果需要扩展新的功能,可能需要创建新的模块,增加了开发工作量。 模板化(Template Method Pattern) 定义 模板方法模式是一种行为设计模式,它在一个方法中定义了一个算法的骨架,而将一些步骤延迟到子类中实现。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法的某些特定步骤。 优势 代码复用:模板方法模式通过抽象类定义公共的算法框架,子类只需要实现具体的步骤,减少了重复代码。 扩展性强:子类可以通过覆盖父类的方法来扩展算法的具体实现,而不需要修改算法的整体结构。 控制反转:模板方法模式将控制权交给父类,子类只需要实现具体的细节,提高了代码的可维护性和可扩展性。 一致性:模板方法模式确保了算法的一致性,所有子类都遵循相同的算法结构,减少了出错的可能性。 劣势 继承滥用:过度使用继承可能导致类层次结构复杂,难以管理和维护。 灵活性受限:模板方法模式的算法结构固定,如果需要改变算法的整体结构,可能需要修改模板方法,影响所有子类。 代码膨胀:为了实现模板方法,可能需要创建多个抽象类和子类,增加了代码量。 对比 高内聚 vs 模板化 特性 高内聚 模板化 定义 模块内部的各个组成部分紧密相关,共同完成一个明确的功能 在一个方法中定义了一个算法的骨架,而将一些步骤延迟到子类中实现 优势

  • 易于理解和维护
    - 降低耦合度
    - 提高重用性
    - 减少错误
  • 代码复用
    - 扩展性强
    - 控制反转
    - 一致性 劣势
  • 过度细分
    - 灵活性受限
  • 继承滥用
    - 灵活性受限
    - 代码膨胀 适用场景 高内聚:适用于功能单一、模块化的系统设计,特别是在需要提高代码可读性和可维护性的场景中。 模板化:适用于需要定义固定算法框架,但允许子类灵活实现具体步骤的场景,特别是在需要代码复用和扩展性的场景中。 结论 高内聚和模板化各有优劣,选择哪种模式取决于具体的应用场景和需求。高内聚适合于模块化设计,强调代码的可读性和可维护性;模板化适合于定义固定的算法框架,强调代码的复用性和扩展性。在实际开发中,可以根据项目的具体情况综合运用这两种模式,以达到最佳的设计效果。

自己项目:

FLutter的配置

sdk: '>=2.19.6 <3.0.0'
flutter: "3.7.12-ohos"

算法

  1. 快速排序 (Quick Sort) 原理 快速排序是一种分治算法,通过选择一个“基准”元素,将数组分为两部分,一部分小于基准,另一部分大于基准,然后递归地对这两部分进行排序。

  2. 二分查找 (Binary Search) 原理 二分查找是一种在有序数组中查找特定元素的算法。通过不断将搜索区间分成两半,逐步缩小搜索范围,直到找到目标元素或确定目标元素不存在。

  3. 使用快慢指针(Floyd 判圈算法)来检测链表中是否存在环。快指针每次移动两步,慢指针每次移动一步,如果存在环,快指针最终会追上慢指针。

    public boolean hasCycle(ListNode head) {
        if (head == null || head.next == null) {
            return false;
        }
        ListNode slow = head;
        ListNode fast = head.next;
        while (fast != null && fast.next != null) {
            if (slow == fast) {
                return true;
            }
            slow = slow.next;
            fast = fast.next.next;
        }
        return false;
    }

4.将数组中的所有零移动到末尾,同时保持非零元素的相对顺序。可以使用双指针法,一个指针用于遍历数组,另一个指针用于记录非零元素的位置。

1.1 反转链表

    int val;
    ListNode next;
    ListNode(int x) { val = x; }
}

public class ReverseLinkedList {
    public ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode current = head;
        while (current != null) {
            ListNode nextTemp = current.next;
            current.next = prev;
            prev = current;
            current = nextTemp;
        }
        return prev;
    }

    public static void main(String[] args) {
        ListNode head = new ListNode(1);
        head.next = new ListNode(2);
        head.next.next = new ListNode(3);
        head.next.next.next = new ListNode(4);

        ReverseLinkedList solution = new ReverseLinkedList();
        ListNode reversedHead = solution.reverseList(head);

        while (reversedHead != null) {
            System.out.print(reversedHead.val + " ");
            reversedHead = reversedHead.next;
        }
    }
}

2.1 两数之和 原理 给定一个整数数组和一个目标值,找出数组中和为目标值的两个数。可以使用哈希表来存储已经遍历过的数及其索引,以便快速查找。

    public int[] twoSum(int[] nums, int target) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            int complement = target - nums[i];
            if (map.containsKey(complement)) {
                return new int[] { map.get(complement), i };
            }
            map.put(nums[i], i);
        }
        throw new IllegalArgumentException("No two sum solution");
    }

    public static void main(String[] args) {
        int[] nums = {2, 7, 11, 15};
        int target = 9;
        TwoSum solution = new TwoSum();
        int[] result = solution.twoSum(nums, target);
        System.out.println("Indices: " + result[0] + ", " + result[1]);
    }
}

找出list中唯一没有出现两次的数

异或运算有一个重要的性质:任何数与自身异或结果为0,任何数与0异或结果为其本身。因此,如果列表中有且仅有一个数字不重复,所有数字的异或结果就是这个唯一的不重复数字。

    var unique = 0
    for (num in nums) {
        unique = unique xor num
    }
    return unique
}

题目:实现一个函数,将输入的字符串反转。 使用双指针法,从两端向中间交换字符 public String reverseString(String s) { char[] chars = s.toCharArray(); int left = 0, right = chars.length - 1; while (left < right) { char temp = chars[left]; chars[left] = chars[right]; chars[right] = temp; left++; right--; } return new String(chars); }

kotlin java npe

线程的状态,阻塞状态的区别
view 的事件传递,RecycleView 从点击到滑动到抬手的流程

Handle 怎么

Glide 中间退出,Glide的生命周期是怎么管理的

自定义view onDraw 一定会执行么?里面是怎么分发的

OKHttp 拦截器都有哪些?  Cer这个东西是怎么获取的




OKHttp 网络协议

网络分发

进程间通信 为什么使用binder 还有那些进程间通信方式 有什么趋势