rokevin
移动
前端
语言
  • 基础

    • Linux
    • 实施
    • 版本构建
  • 应用

    • WEB服务器
    • 数据库
  • 资讯

    • 工具
    • 部署
开放平台
产品设计
  • 人工智能
  • 云计算
计算机
其它
GitHub
移动
前端
语言
  • 基础

    • Linux
    • 实施
    • 版本构建
  • 应用

    • WEB服务器
    • 数据库
  • 资讯

    • 工具
    • 部署
开放平台
产品设计
  • 人工智能
  • 云计算
计算机
其它
GitHub
  • 小米 面试

  • Java 静态方法和普通方法的区别
  • 链表和数组的区别,数组插入怎么做
  • Java 四种引用类型,强弱软虚
  • Final 关键字都修饰哪些地方,有什么作用
  • Public Private Protected Default 区别
  • String、StringBuilder、StringBuffer 区别
  • Equals 和 == 的区别
  • 什么是类对象,类对象什么时候加载,如何精确控制类中某个块的加载
  • 双亲委派机制,举个例子,如果自己写了个跟 java 提供的一模一样的(包名类名都一样)的类,为什么虚拟机可以正确加载系统提供的
  • JVM 区域、静态变量存在哪里
  • JVM 垃圾回收机制,GC 算法、为什么会有 STW,那些对象作为 GCRoot
  • Java 如何实现多线程,锁有哪些,原理
  • 多线程,wait 和 sleep 区别,两个线程分别打印 log
  • 说说常用线程池,线程池比线程好在哪
  • 讲讲线程池种类
  • 种类线程池原理
  • 进程间通信的方式,进程通信机制有哪些,ipc 在什么情况下用 aidl
  • Java 面向对象的三大特性
  • 设计模式 / 工厂模式了解吗,策略模式和状态模式的各自的特点和区别
  • 重载和重写的区别,静态方法能被重写嘛
  • 抽象类和接口区别、什么时候用它们
  • 单例的两个关键字的作用
  • Android 四大组件,及其作用,说说安卓分四大组件这种模块的原因
  • Activity 四种启动模式
  • onSaveInstanceState 作用
  • 用过什么 layout,介绍一下它们的区别
  • ListView 和 RecyclerView 区别
  • ViewPager 缓存 / 预加载
  • Android 广播的类型
  • Intent 传递数据
  • 生命周期,两个 Activity 启动的回调,如果把一个应用进程直接 kill 掉,生命周期会怎么走
  • 常用的多线程工具类在 Android 中的应用(如 Handler、AsyncTask 等),Android 多线程实现方式
  • 说说 Handler 机制
  • Handler 引起的内存泄露,当 messagequeue 空的时候,Handler 怎么进行阻塞的,Handler 原理,Handler 是如何实现延时的
  • 内存泄漏检测方案,常见内存泄漏有哪些
  • View 的 60ms 刷新机制了解,怎么检测 View 卡顿
  • ANR 如何定位和监控
  • View 绘制流程 /requestLayout/invalidate,measure、layout、draw 流程,滑动冲突
  • 事件分发,讲完流程之后问的比较深入。如何让事件进行双向传递,比如子 View 已经消费了的事件怎么传回到 VG 去
  • 介绍一下 Binder 和原理
  • AIDL 了解过吗
  • 二叉树有哪些遍历方式,二叉树的层次遍历,从最下面一层出发
  • 数组链表的区别
  • 说说 HashMap 结构
  • HashMap key 的存储位置怎么计算
  • HashMap,Hashtable,ConcurrentHashMap 区别
  • 说说 ConcurrentHashMap 分段锁详解
  • HashMap 如何解决 hash 冲突
  • ArrayList 动态扩容过程,ArrayList LinkedList 区别
  • 链表和数组的区别,数组插入怎么做
  • 说说 https 过程,HTTPS 建立连接和传送数据的过程,https 数据是怎么加密的
  • 对称加密和非对称加密区别,常见算法
  • HTTP 与 HTTPS 的区别
  • TCP 三次握手过程,懂 TCP,那讲一下三次握手,TCP 三次握手
  • TCP/UDP 的区别,UDP 有哪些应用场景,如何安全传输
  • 网络分层模型,TCP、UDP 讲一讲
  • Get 和 Post 有什么区别
  • 常用的网络框架,用过的图片加载框架
  • 简单说几个 Linux 命令,Linux dump 相关内容
  • 正则表达式,写个正则把数字从字符串中提取出来
  • MVP 讲一下
  • 看过哪些开源库源码,如 eventbus 和 glide
  • EventBus 源码分析:
  • Glide 源码分析:
  • 自定义 View 主要重写哪个方法
  • onMeasure 方法: 这个方法主要用于确定自定义 View 的大小。当父视图(ViewGroup)需要知道子视图的大小时,会调用子视图的 onMeasure 方法。在该方法中,需要根据自身的需求和父视图提供的约束条件来计算出合适的宽和高。例如,如果是一个自定义的圆形 View,可能需要根据父视图分配的空间以及自身设定的半径等因素来确定最终的尺寸。如果不重写 onMeasure 方法,可能会导致 View 的大小不符合预期,比如显示不完整或者占用过多不必要的空间。
  • onLayout 方法: 主要用于确定自定义 View 在父视图中的位置。当 onMeasure 阶段确定了 View 的大小后,onLayout 阶段会根据父视图的布局方式以及自身的大小来安排自己的位置。比如在一个自定义的布局容器中,有多个自定义 View,每个 View 都需要通过 onLayout 方法来明确自己相对于其他 View 和父视图的位置关系。不过,并非所有自定义 View 都需要重写 onLayout 方法,只有当 View 的位置需要根据特定规则进行动态调整或者在特定布局环境下确定时才需要重写。
  • onDraw 方法: 这是用于将自定义 View 的内容绘制到屏幕上的方法。在 onDraw 中,可以使用 Android 提供的绘图工具(如 Canvas、Paint 等)来绘制各种形状、文本、图像等。例如,要绘制一个自定义的图表 View,就需要在 onDraw 方法中根据数据和设计要求,使用 Canvas 绘制线条、柱状图等各种图形元素,并使用 Paint 来设置颜色、线条宽度等属性。如果没有重写 onDraw 方法,View 将不会显示出任何自定义的内容,只会显示默认的空白或者继承自父类的简单外观。
  • 触摸的传递机制
  • dispatchTouchEvent 方法: 这是触摸事件分发的核心方法,存在于所有的视图(View 和 ViewGroup)中。它的主要作用是接收触摸事件并决定如何处理或继续传递该事件。对于 ViewGroup 来说,当触摸事件到达时,它会首先通过 dispatchTouchEvent 方法来判断是否要拦截该事件。如果在 ViewGroup 的 dispatchTouchEvent 方法中返回 true,表示拦截了触摸事件,那么后续的触摸事件处理流程将在该 ViewGroup 内部进行,不会再继续向下传递给子视图。如果返回 false,则会继续将触摸事件向下传递给子视图,通过调用子视图的 dispatchTouchEvent 方法来继续处理。
  • onInterceptTouchEvent 方法: 这个方法只存在于 ViewGroup 中,它是 ViewGroup 决定是否拦截触摸事件的关键方法。当触摸事件到达 ViewGroup 时,onInterceptTouchEvent 方法会被调用。如果在该方法中返回 true,就意味着该 ViewGroup 拦截了触摸事件,之后会通过该 ViewGroup 自己的 onTouchEvent 方法来处理触摸事件。如果返回 false,则会继续将触摸事件向下传递给子视图,让子视图有机会处理触摸事件。例如,在一个可滑动的列表视图(ListView)中,当用户在列表项上进行触摸操作时,如果列表视图的父视图(可能是一个包含 ListView 的布局容器)通过 onInterceptTouchEvent 方法拦截了触摸事件,那么列表项就无法再处理该触摸事件,而是由父视图来处理。
  • onTouchEvent 方法: 这是用于处理触摸事件的方法,存在于所有的视图中。当一个视图接收到触摸事件并且没有被父视图拦截时,就会通过 onTouchEvent 方法来处理该事件。例如,一个按钮(Button)在没有被父视图拦截触摸事件的情况下,当用户点击按钮时,按钮会通过 onTouchEvent 方法来处理点击事件,比如触发相应的点击操作(如启动一个 Activity、执行一段代码等)。
  • Java 实现生产者,消费者
  • Lock 和 syncrognized 原理区别,适合什么场景
  • 原理区别:
  • synchronized 关键字: synchronized 是 Java 内置的一种锁机制,它基于对象头中的锁标志位和监视器(Monitor)来实现同步。当一个线程进入一个 synchronized 方法或者代码块时,它会获取对象的监视器锁。这个监视器锁实际上是一个对象关联的特殊结构,它记录了锁的状态等信息。
  • Lock 接口及其实现类(如 ReentrantLock): Lock 接口提供了比 synchronized 更灵活的锁机制。它是通过 AQS(AbstractQueuedSynchronizer)来实现的。AQS 是一个抽象的同步框架,它维护了一个等待队列,用于存储等待获取锁的线程。
  • 适合场景:
  • synchronized 适合的场景:
  • Lock 适合的场景:

小米 面试

Java 静态方法和普通方法的区别

静态方法是属于类的方法,而普通方法是属于对象的方法。

从调用方式来看,静态方法可以通过类名直接调用,不需要创建类的实例。例如,如果有一个类叫做 MyClass,其中有一个静态方法 staticMethod,就可以通过 MyClass.staticMethod () 来调用。而普通方法必须先创建类的实例,比如 MyClass myObj = new MyClass (); 然后通过 myObj.normalMethod () 来调用。

在内存分配方面,静态方法在类加载的时候就会被分配内存空间,并且只会分配一次。这是因为静态方法与类的实例无关,它的代码存储在静态存储区。普通方法则是在对象创建的时候,作为对象的一部分存储在堆内存中。每个对象都有自己独立的普通方法副本(在方法没有被共享的情况下)。

从访问权限上,静态方法只能访问静态成员变量和其他静态方法。这是因为非静态成员变量和方法是和对象实例相关联的,在没有对象实例的情况下,静态方法无法确定要访问哪个对象的非静态成员。普通方法可以访问静态和非静态的成员变量和方法。例如:

class MyClass {
    private static int staticVar = 10;
    private int nonStaticVar = 20;
    public static void staticMethod() {
        System.out.println(staticVar); 
        // 正确,能访问静态变量
    }
    public void normalMethod() {
        System.out.println(staticVar); 
        // 正确,能访问静态变量
        System.out.println(nonStaticVar); 
        // 正确,能访问非静态变量
    }
}

在继承关系中,静态方法不能被重写。如果子类定义了一个与父类静态方法签名相同的静态方法,这并不是重写,而是在子类中重新定义了一个新的静态方法。普通方法可以被重写,重写后的方法会根据对象的实际类型来调用相应的方法版本。例如:

class Parent {
    public static void staticMethod() {
        System.out.println("Parent static method");
    }
    public void normalMethod() {
        System.out.println("Parent normal method");
    }
}
class Child extends Parent {
    public static void staticMethod() {
        System.out.println("Child static method");
    }
    public void normalMethod() {
        System.out.println("Child normal method");
    }
}
public class Main {
    public static void main(String[] args) {
        Parent.staticMethod(); 
        // 输出: Parent static method
        Parent p = new Child();
        p.normalMethod(); 
        // 输出: Child normal method
    }
}

静态方法适用于那些不需要访问对象状态,只与类本身相关的操作,比如工具类中的方法。例如,数学计算工具类中的求平方根方法,它不需要依赖于任何对象的状态,就可以定义为静态方法。普通方法则用于操作对象的状态,根据对象的具体属性来完成相应的行为。

链表和数组的区别,数组插入怎么做

链表和数组是两种不同的数据结构。

数组是一种连续存储的数据结构,它在内存中是一块连续的空间。其优点在于可以通过索引快速地访问元素,访问时间复杂度是 O (1)。例如,对于一个整数数组 int [] arr = {1, 2, 3, 4, 5}; 要访问第三个元素,可以直接通过 arr [2] 来获取,非常高效。但是数组的大小是固定的,一旦创建就很难改变容量。如果要插入或删除元素,尤其是在数组中间插入或删除元素效率较低。

在数组中间插入元素时,需要将插入位置之后的元素依次向后移动。假设要在上述数组的索引为 2 的位置插入一个数字 6。首先要创建一个新的足够大的数组(如果原数组已满),然后从最后一个元素开始,将索引大于等于 2 的元素往后移动一位,即先将 arr [4] 移动到 arr [5],arr [3] 移动到 arr [4],arr [2] 移动到 arr [3],最后将 6 赋值给 arr [2]。

链表则是由节点组成,每个节点包含数据和指向下一个节点的引用。链表的内存空间不一定是连续的。它的优点是插入和删除操作相对灵活,插入或删除一个节点只需要改变相关节点的引用即可。例如,对于一个简单的单向链表,有节点 A、B、C,要在 A 和 B 之间插入一个节点 D,只需要将 A 的 next 引用指向 D,然后将 D 的 next 引用指向 B 即可。但链表的缺点是访问元素效率较低,因为要访问某个节点,需要从链表头开始逐个遍历节点,时间复杂度是 O (n)。

Java 四种引用类型,强弱软虚

在 Java 中,有四种引用类型,分别是强引用、软引用、弱引用和虚引用。

强引用是最常见的引用类型。例如,当我们使用 “Object obj = new Object ();” 这样的语句时,obj 就是对新建对象的强引用。只要强引用存在,垃圾回收器就不会回收被引用的对象。这是因为强引用表明程序的逻辑中还在使用这个对象,它保证了对象在内存中的存在。比如在一个正在运行的方法中创建了一个局部变量引用一个对象,只要这个方法没有结束,这个对象就不会被回收。

软引用是一种相对温和的引用。它主要用于内存敏感的场景。当内存空间足够时,垃圾回收器不会回收软引用所指向的对象;但当内存不足时,就会回收这些对象。软引用通常用于实现缓存机制。比如一个图片缓存系统,当内存充足时,缓存的图片对象可以通过软引用保留在内存中,方便快速访问;当内存紧张时,这些软引用对象就可以被回收,为其他重要的程序部分腾出空间。

弱引用比软引用更容易被垃圾回收。一旦垃圾回收机制开始工作,只要发现了弱引用对象,就会将其回收。弱引用主要用于一些非关键的对象引用场景,例如在容器类中,如果希望容器中的对象在没有其他强引用时能被及时回收,就可以使用弱引用。比如在一个哈希表中,如果键是弱引用,当键所引用的对象没有其他强引用时,垃圾回收器就会回收这个键,从而避免哈希表中出现大量无用的键值对。

虚引用是最 “虚” 的一种引用。虚引用的主要目的是在对象被回收时能收到一个系统通知。虚引用对象本身不会对垃圾回收产生任何影响,它主要用于跟踪对象被垃圾回收的状态。例如,在一些资源回收管理系统中,当一个对象被回收时,通过虚引用可以触发一些清理资源的操作,如关闭文件句柄、释放网络连接等。

Final 关键字都修饰哪些地方,有什么作用

Final 关键字可以修饰类、方法和变量。

当 final 修饰类时,这个类就不能被继承。例如,Java 中的 String 类就是 final 类,这意味着不能创建一个继承自 String 类的子类。这样做的目的主要是为了保证类的完整性和安全性。对于一些核心的、不希望被修改或扩展的类,使用 final 关键字可以防止其他开发者对其继承后进行不恰当的修改。

当 final 修饰方法时,这个方法不能被重写。例如,在一个父类中有一个 final 方法,在子类中就不能重新定义这个方法。这对于确保方法的行为在继承体系中保持一致很有帮助。比如在一个工具类中,有一些核心的计算方法,不希望子类改变这些方法的实现,就可以将这些方法定义为 final。

当 final 修饰变量时,变量就变成了常量。如果是基本数据类型,变量的值在初始化后就不能再改变;如果是引用数据类型,变量所引用的对象不能再重新赋值,但对象内部的属性可以改变。例如,对于 final int num = 10; 这个 num 的值在定义后就不能被修改。而对于 final ArrayList<String> list = new ArrayList<>(); 不能将 list 重新赋值为其他的 ArrayList 对象,但可以对 list 进行添加、删除等操作。

Public Private Protected Default 区别

在 Java 中,public、private、protected 和默认(没有修饰符)这几种访问修饰符用于控制类、成员变量和方法的访问权限。

public 是最开放的访问修饰符。使用 public 修饰的类、方法或变量可以在任何其他类中访问,无论是同一个包内还是不同包内。例如,一个公共的工具类中的公共方法,任何其他类都可以调用这个方法来完成相应的功能。对于类来说,通常只有公共的类才可以被其他包中的类访问和使用。

private 是最严格的访问修饰符。被 private 修饰的成员变量和方法只能在所属的类内部访问。这对于封装数据非常有用。例如,在一个类中有一个 private 的成员变量用来存储用户密码,只有在这个类内部的方法可以访问和修改这个密码变量,外部类无法直接访问,从而保证了数据的安全性。

protected 修饰符的访问权限介于 public 和 private 之间。被 protected 修饰的成员可以在同一包内的其他类中访问,也可以在不同包的子类中访问。这在继承关系中很有用。例如,在一个父类中有一个 protected 的方法,子类可以访问和重写这个方法,同时在同一个包内的其他类也可以访问这个方法。

默认访问修饰符(没有显式写任何修饰符)的访问权限是包访问权限。这意味着在同一个包内的类可以访问这些没有修饰符的成员,而不同包的类则无法访问。这种访问修饰符在一些只需要在包内共享的代码中比较有用,比如一些内部的工具类或辅助类,它们的功能只需要在包内的其他类中使用,不需要对外公开。

String、StringBuilder、StringBuffer 区别

String 是不可变的字符序列。一旦一个 String 对象被创建,它的值就不能被改变。例如,当执行 “String str = "hello"; str = str + "world";” 这样的操作时,实际上并不是在原来的 “hello” 字符串对象上添加 “ world”,而是创建了一个新的 String 对象来存储 “hello world”。这种不可变性使得 String 在多线程环境下是安全的,因为它的值不会被其他线程意外修改。同时,由于其不可变性,也使得它在一些需要常量的场景中很有用,比如作为方法的参数传递或者存储配置信息等。

StringBuilder 是可变的字符序列,它是非线程安全的。它的主要优点是在进行字符串拼接等操作时效率较高。例如,当需要动态地构建一个较长的字符串,如在循环中拼接字符串,使用 StringBuilder 会比使用 String 效率高很多。因为 StringBuilder 可以直接在原有的字符序列上进行修改,而不是像 String 那样每次拼接都创建新的对象。

StringBuffer 也是可变的字符序列,但它是线程安全的。它和 StringBuilder 的功能类似,主要区别在于 StringBuffer 的方法是同步的,这使得它在多线程环境下可以安全地使用。不过,由于同步机制会带来一定的性能开销,在单线程环境下,StringBuilder 的性能通常会优于 StringBuffer。例如,在一个单线程的日志记录系统中,使用 StringBuilder 来构建日志信息会更快;而在一个多线程的服务器应用中,对于共享的字符串操作部分,使用 StringBuffer 可以避免数据不一致的问题。

Equals 和 == 的区别

在 Java 中,“==” 和 “equals” 都用于比较,但它们有很大的区别。

“==” 是比较运算符,它主要用于比较两个变量的值是否相等。如果是基本数据类型,它比较的是数值。例如,对于 int a = 10; int b = 10; 那么 a == b 会返回 true,因为它们的值相等。如果是引用数据类型,“==” 比较的是两个引用是否指向同一个对象。例如,Object obj1 = new Object (); Object obj2 = new Object (); 那么 obj1 == obj2 会返回 false,因为 obj1 和 obj2 指向的是两个不同的对象,即使这两个对象的内容可能是相同的。

“equals” 方法主要用于比较对象的内容是否相等。在 Java 中,所有的类都继承自 Object 类,Object 类中的 equals 方法默认实现是比较引用是否相同,即和 “==” 的效果一样。但是很多类会重写 equals 方法来定义自己的比较逻辑。例如,String 类就重写了 equals 方法,用于比较两个字符串的内容是否相同。对于 String str1 = "hello"; String str2 = "hello"; 那么 str1.equals (str2) 会返回 true,因为它们的内容相同,尽管 str1 和 str2 可能是不同的对象引用。

什么是类对象,类对象什么时候加载,如何精确控制类中某个块的加载

类对象是 Java 中类在内存中的一种表示形式。当一个类被加载到 JVM(Java 虚拟机)中时,JVM 会为这个类创建一个对应的类对象。这个类对象包含了类的各种信息,比如类的方法、变量、构造函数等的定义,它就像是这个类在内存中的一个蓝图或者模板。

类的加载时机主要有以下几种情况。首先,当创建类的第一个实例时,类会被加载。例如,当执行 “new MyClass ();”(假设 MyClass 是一个自定义类)这样的语句时,如果 MyClass 还没有被加载,JVM 就会去加载它。其次,当访问类的静态成员(静态变量或者静态方法)时,类也会被加载。比如,有一个类有一个静态变量 “public static int staticVar;”,当执行 “System.out.println (MyClass.staticVar);” 时,MyClass 就会被加载。

要精确控制类中某个块的加载,可以使用 Java 的静态代码块。静态代码块在类加载的时候执行,并且只执行一次。例如,在一个类中定义一个静态代码块 “static { System.out.println ("这个静态代码块在类加载时执行"); }”。通过合理地安排静态代码块中的内容,可以对类加载过程中的一些初始化操作进行精确控制。比如在静态代码块中初始化一些静态变量,或者加载一些外部资源等。而且可以通过条件判断来决定是否执行某些初始化操作。例如,根据系统环境变量或者配置文件中的参数来判断是否加载某个数据库驱动,在静态代码块中可以这样写:“if (System.getProperty ("enableDbDriver")!= null && System.getProperty ("enableDbDriver").equals ("true")) { // 加载数据库驱动的代码 }”。这样就可以根据具体的条件来控制类中与数据库驱动加载相关的部分是否在类加载时执行。

双亲委派机制,举个例子,如果自己写了个跟 java 提供的一模一样的(包名类名都一样)的类,为什么虚拟机可以正确加载系统提供的

双亲委派机制是 Java 类加载器的一种重要机制。它的核心思想是当一个类加载器收到类加载请求时,它首先不会自己去尝试加载这个类,而是把这个请求委托给它的父类加载器。只有当父类加载器反馈无法完成这个加载请求时,子加载器才会自己去尝试加载。

例如,有三个主要的类加载器,分别是启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)和应用程序类加载器(Application ClassLoader)。当要加载一个类时,比如 java.util.ArrayList,应用程序类加载器首先会把加载请求委托给扩展类加载器,扩展类加载器又会把请求委托给启动类加载器。启动类加载器负责加载 Java 的核心类库,它会检查自己的加载范围是否包含这个类,如果包含就进行加载;如果不包含,就把请求返回给扩展类加载器。扩展类加载器也会检查自己的加载范围,如此类推,直到找到可以加载这个类的类加载器或者所有类加载器都无法加载这个类。

如果自己写了一个和 Java 提供的一模一样(包名类名都一样)的类,比如自己写了一个 java.util.ArrayList。当尝试加载这个类时,应用程序类加载器会按照双亲委派机制,先把请求委托给父加载器。启动类加载器会加载真正的 Java 核心库中的 java.util.ArrayList,因为它的加载范围包括了这个类。当它加载成功后,就不会再让子加载器去加载同名的类,这样就保证了系统提供的类被正确加载,避免了自定义类对系统核心类的干扰。

JVM 区域、静态变量存在哪里

JVM 主要分为以下几个区域。首先是程序计数器(Program Counter Register),它是一块较小的内存空间,用于记录当前线程所执行的字节码指令的行号指示器。它是线程私有的,因为每个线程都有自己独立的执行流程。

然后是 Java 虚拟机栈(Java Virtual Machine Stacks),也是线程私有的。它主要用于存储局部变量表、操作数栈、动态链接、方法出口等信息。当一个方法被调用时,一个栈帧就会被压入栈中,这个栈帧包含了这个方法执行所需的信息,当方法执行结束后,栈帧就会被弹出。

堆(Heap)是 JVM 中最大的一块内存区域,它是被所有线程共享的。所有的对象实例以及数组都在堆中分配内存。这是因为对象的生命周期和使用情况比较复杂,通过堆来统一管理可以更好地进行内存分配和回收。

方法区(Method Area)也是所有线程共享的。它主要用于存储已被加载的类信息、常量、静态变量、即时编译器编译后的代码等。静态变量就存储在方法区中。这是因为静态变量是属于类的,而不是属于某个具体的对象实例。它在类加载的时候就会被分配内存,并且在整个类的生命周期内都存在,和类的实例数量无关。所以把静态变量存储在方法区这种共享的、相对稳定的区域比较合适。

JVM 垃圾回收机制,GC 算法、为什么会有 STW,那些对象作为 GCRoot

JVM 的垃圾回收(GC)机制是用于自动管理内存,回收那些不再被程序使用的对象所占用的内存空间。

常见的 GC 算法有标记 - 清除算法(Mark - Sweep)。这个算法分为两个阶段,首先是标记阶段,从根对象(GCRoot)开始,通过遍历对象引用关系,标记所有可达的对象;然后是清除阶段,清理那些没有被标记的对象。这种算法的优点是实现简单,缺点是会产生内存碎片。例如,在经过多次标记 - 清除操作后,内存空间可能会被分割成很多小的碎片,导致后续分配较大对象时可能找不到足够连续的空间。

还有复制算法(Copying),它将内存空间分为两个大小相等的区域,每次只使用其中一个区域。当进行垃圾回收时,把存活的对象复制到另一个区域,然后把原来的区域全部清理掉。这种算法的优点是不会产生内存碎片,但是它的缺点是内存利用率不高,因为始终有一半的内存空间处于闲置状态。

标记 - 整理算法(Mark - Compact)结合了标记 - 清除和复制算法的优点。在标记阶段标记出所有存活的对象,然后将存活对象向一端移动,最后清理掉边界以外的内存空间。

JVM 会有 “Stop - The - World(STW)” 的情况。这是因为在垃圾回收过程中,尤其是在标记阶段,为了保证对象引用关系的准确性,需要暂停所有的用户线程。例如,当标记对象是否可达时,如果用户线程还在不断地修改对象引用关系,就可能导致标记错误,所以需要暂停线程来确保标记过程的准确性。

作为 GCRoot 的对象主要包括以下几种。首先是虚拟机栈(栈帧中的本地变量表)中引用的对象,因为栈帧是和线程执行密切相关的,当前线程正在使用的对象应该是存活的。其次是方法区中类静态属性引用的对象,因为静态变量在整个类的生命周期内都可能被使用,所以引用的对象也是存活的。还有就是本地方法栈中 JNI(Java Native Interface)引用的对象,这些对象和本地方法的调用有关,也是可能正在被使用的。

Java 如何实现多线程,锁有哪些,原理

在 Java 中,可以通过多种方式实现多线程。一种常见的方式是继承 Thread 类。例如,创建一个类继承自 Thread 类,并重写 run 方法。在 run 方法中定义线程要执行的任务。然后通过创建这个类的实例,再调用 start 方法来启动线程。比如:

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程执行的内容");
    }
}

然后在主程序中可以通过 “new MyThread ().start ();” 来启动线程。

另一种方式是实现 Runnable 接口。定义一个类实现 Runnable 接口,实现 run 方法。然后通过创建 Thread 类的实例,将实现 Runnable 接口的对象作为参数传入 Thread 的构造函数,再调用 start 方法来启动线程。这种方式的好处是可以避免单继承的限制。例如:

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("通过实现Runnable接口的线程执行内容");
    }
}

在主程序中可以通过 “Thread thread = new Thread (new MyRunnable ()); thread.start ();” 来启动线程。

Java 中的锁主要有以下几种。首先是 synchronized 关键字。它可以用于修饰方法或者代码块。当用于修饰方法时,这个方法在同一时刻只能被一个线程访问。例如,有一个类中有一个 synchronized 方法,当一个线程进入这个方法后,其他线程就不能进入,直到这个线程执行完这个方法。当用于修饰代码块时,需要指定一个对象作为锁对象。例如,“synchronized (this) { // 代码块内容 }”,这里的 this 是锁对象,只有获取到这个对象的锁的线程才能执行代码块中的内容。

另一种锁是 ReentrantLock。它是一个可重入锁,提供了比 synchronized 更灵活的锁机制。例如,它可以实现公平锁和非公平锁。公平锁是指按照线程请求锁的顺序来分配锁,非公平锁则不保证这个顺序。通过 ReentrantLock 的构造函数可以指定是公平锁还是非公平锁。它还提供了一些方法,如 lock 方法用于获取锁,unlock 方法用于释放锁。在使用 ReentrantLock 时,需要在 try - catch - finally 块中正确地获取和释放锁,以避免死锁等问题。例如:

import java.util.concurrent.locks.ReentrantLock;
class MyLockExample {
    private ReentrantLock lock = new ReentrantLock();
    public void doSomething() {
        lock.lock();
        try {
            // 执行需要加锁的操作
        } catch (Exception e) {
            // 处理异常
        } finally {
            lock.unlock();
        }
    }
}

锁的原理主要是通过对共享资源的访问控制来实现线程安全。当一个线程获取了锁,就相当于获得了对共享资源的独占访问权。其他线程如果想要访问这个共享资源,就必须等待锁被释放。对于 synchronized 关键字,它是基于对象头中的锁标志位和监视器(Monitor)来实现的。当一个线程进入一个 synchronized 方法或者代码块时,它会获取对象的监视器锁,这个监视器会记录锁的状态等信息。对于 ReentrantLock,它是通过 AQS(AbstractQueuedSynchronizer)来实现的,AQS 是一个抽象的同步框架,它维护了一个等待队列,用于存储等待获取锁的线程,通过复杂的状态管理和队列操作来实现公平和非公平的锁机制。

多线程,wait 和 sleep 区别,两个线程分别打印 log

多线程是指在一个程序中同时运行多个线程来执行不同的任务,这样可以提高程序的执行效率和资源利用率。例如,在一个文件下载程序中,可以使用一个线程来负责从网络获取数据,另一个线程来负责将数据写入本地磁盘,这两个线程可以同时工作,加快下载速度。

wait 和 sleep 是在多线程编程中用于控制线程执行节奏的方法,但它们有很大的区别。

sleep 是 Thread 类的静态方法,它会让当前线程暂停执行一段时间。这个时间是由开发者指定的,例如 “Thread.sleep (1000);” 会让线程暂停 1 秒。在 sleep 期间,线程不会释放它占有的锁(如果有的话),只是暂时停止执行,时间一到就会自动恢复执行。而且它不会受到其他线程的影响,单纯是基于时间的等待。

wait 是 Object 类的方法,它用于线程间的协作。当一个线程调用某个对象的 wait 方法时,这个线程会释放该对象的锁,然后进入等待状态,直到其他线程调用这个对象的 notify 或者 notifyAll 方法来唤醒它。例如,在生产者 - 消费者模型中,当仓库已满时,生产者线程可以调用仓库对象的 wait 方法等待仓库有空间,消费者消费了产品后,会调用仓库对象的 notify 方法来唤醒生产者。

对于两个线程分别打印 log,假设我们有两个线程,线程 A 和线程 B。可以通过实现 Runnable 接口或者继承 Thread 类来创建这两个线程。以实现 Runnable 接口为例,首先创建两个实现 Runnable 接口的类,在它们的 run 方法中编写打印 log 的代码。比如:

class ThreadA implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("线程A: " + i);
        }
    }
}
class ThreadB implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("线程B: " + i);
        }
    }
}

然后在主程序中分别启动这两个线程:

public class Main {
    public static void main(String[] args) {
        Thread threadA = new Thread(new ThreadA());
        Thread threadB = new Thread(new ThreadB());
        threadA.start();
        threadB.start();
    }
}

这样,线程 A 和线程 B 就会并发地执行,它们的 log 会交替地打印出来,具体的打印顺序取决于线程调度情况。

说说常用线程池,线程池比线程好在哪

常用的线程池有以下几种。

首先是 FixedThreadPool(固定大小线程池),它会创建一个固定数量线程的线程池。例如,当创建一个大小为 5 的 FixedThreadPool 时,线程池会一直保持 5 个线程在运行。它适用于处理负载比较稳定的任务,比如服务器接收固定数量客户端请求的场景。

CachedThreadPool(可缓存线程池),它会根据任务的数量来动态地创建线程。如果线程空闲一段时间(默认 60 秒),就会被回收。这种线程池适合处理大量短期的异步任务,比如网络爬虫中短时间内大量的页面请求任务。

ScheduledThreadPool(定时任务线程池),它主要用于执行定时或者周期性的任务。例如,可以安排一个任务在 10 秒后执行,或者每隔 1 分钟执行一次某个任务。在一些需要定时备份数据或者定时检查系统状态的场景中很有用。

线程池相比直接使用线程有很多优势。

从性能角度来看,线程的创建和销毁是有成本的。创建线程需要分配内存、初始化线程相关的资源等,销毁线程也需要释放资源。而线程池中的线程是可以复用的,避免了频繁地创建和销毁线程所带来的开销,尤其是在处理大量短周期任务时,这种优势更加明显。

从资源管理角度,线程池可以对线程数量进行限制。如果无限制地创建线程,可能会导致系统资源耗尽。例如,过多的线程会占用大量的内存和 CPU 时间片,导致系统性能下降甚至崩溃。线程池可以根据系统资源和任务需求,合理地配置线程数量,保证系统的稳定运行。

从任务调度角度,线程池可以更好地管理和调度任务。例如,可以通过线程池的一些配置参数来决定任务的执行顺序、优先级等,使得任务能够按照预期的方式高效地执行。

讲讲线程池种类

除了前面提到的 FixedThreadPool、CachedThreadPool 和 ScheduledThreadPool,还有以下几种线程池。

SingleThreadExecutor(单线程线程池),这个线程池只有一个线程。它可以保证所有提交的任务按照提交的顺序依次执行,不会出现并发执行的情况。在一些需要顺序处理任务的场景很有用,比如对一些数据进行顺序的格式化操作,或者对一些文件进行顺序的读写操作,使用单线程线程池可以确保任务的执行顺序和数据的完整性。

WorkStealingPool(工作窃取线程池),它是 Java 8 引入的一种线程池。它适合处理那些具有递归性质或者可以分解为子任务的任务。这种线程池内部有多个线程队列,每个线程有自己的任务队列。当一个线程完成自己队列中的任务后,它可以从其他线程的队列中 “窃取” 任务来执行。这种机制可以有效地利用线程资源,提高任务的处理效率。例如,在一些分治算法的实现中,如计算大型矩阵的乘法,可以将矩阵分解为多个子矩阵,每个子任务处理一个子矩阵的乘法,这些子任务可以分配到 WorkStealingPool 的不同线程队列中,当某个线程提前完成自己的任务后,就可以从其他线程队列中获取任务继续执行。

种类线程池原理

FixedThreadPool 的原理是创建一个固定大小的线程集合,这些线程在初始化后就一直存在。当有任务提交到线程池时,线程池会从这些线程中选择一个空闲的线程来执行任务。如果所有线程都在忙碌,新提交的任务会进入一个等待队列,直到有线程空闲出来。这个等待队列是一个阻塞队列,通常采用无界队列(如 LinkedBlockingQueue),这样可以保证任务不会丢失。

CachedThreadPool 的核心是一个线程工厂和一个缓存机制。当有任务到来时,它首先会尝试从缓存的线程中找一个空闲的线程来执行任务。如果没有空闲线程,就会创建一个新的线程。同时,它会对线程进行空闲时间的监控,当一个线程空闲时间超过一定限度(如 60 秒),就会被回收,释放资源。它的缓存线程数量理论上是没有限制的,所以可以快速地响应大量的任务请求,但如果任务持续不断,可能会导致创建过多的线程,占用大量资源。

ScheduledThreadPool 是在普通线程池的基础上增加了定时和周期性执行任务的功能。它内部有一个定时器,当有定时任务提交时,定时器会记录任务的执行时间。当到达执行时间时,任务会被提交到线程池的执行队列中等待执行。对于周期性任务,每次任务执行完后,会根据任务的周期设置重新计算下一次执行时间,并再次将任务放入定时器的任务列表中等待下一次执行。

SingleThreadExecutor 的原理比较简单,它内部只有一个线程。所有任务都在这个线程中按顺序执行。它的任务队列和其他线程池类似,也是一个阻塞队列,任务提交后会在队列中等待线程执行。由于只有一个线程,所以它可以很好地保证任务的顺序性。

WorkStealingPool 的原理基于工作窃取算法。它内部有多个线程,每个线程都有自己的双端队列(deque)来存储任务。当一个线程开始工作时,它会从自己的队列头部获取任务并执行。当自己的队列中的任务执行完后,它会尝试从其他线程的队列尾部 “窃取” 任务。这种从尾部窃取任务的方式是为了尽量减少对其他线程正在执行任务的干扰。同时,这种线程池通常会根据系统的 CPU 核心数来自动调整线程数量,以达到最佳的性能和资源利用。

进程间通信的方式,进程通信机制有哪些,ipc 在什么情况下用 aidl

进程间通信(IPC)有多种方式。

管道(Pipe)是一种简单的进程间通信方式,它分为无名管道和有名管道。无名管道主要用于具有亲缘关系(父子进程)的进程之间通信。例如,在一个父进程创建一个子进程后,可以通过无名管道来传递数据。父进程向管道的一端写入数据,子进程从管道的另一端读取数据。有名管道则可以用于无亲缘关系的进程之间通信,它有一个名字,可以被不同的进程通过名字来访问。

消息队列(Message Queue)是一种消息传递机制。进程可以向消息队列发送消息,也可以从消息队列接收消息。消息队列有一定的格式和顺序,接收消息的进程可以按照消息的类型或者发送时间等顺序来接收消息。例如,在一个分布式系统中,不同的进程可以通过消息队列来传递任务请求和执行结果等信息。

共享内存(Shared Memory)是一种高效的进程间通信方式。多个进程可以共享同一块内存区域,这样它们可以直接对这块内存进行读写操作。但是这种方式需要注意同步和互斥问题,因为多个进程同时访问同一块内存可能会导致数据不一致。例如,在一个多进程的数据库系统中,多个进程可能需要共享一些缓存数据,可以通过共享内存来实现,但需要使用锁等机制来保证数据的正确性。

信号量(Semaphore)主要用于进程间的同步和互斥。它可以看作是一个计数器,用于控制对共享资源的访问。例如,在一个多进程的文件读写系统中,为了防止多个进程同时对一个文件进行写操作导致文件损坏,可以使用信号量来限制同时访问文件的进程数量。

套接字(Socket)通信可以用于不同主机之间的进程通信。它基于网络协议,如 TCP/IP 协议。例如,在一个客户端 - 服务器架构的应用中,客户端进程和服务器进程可以通过套接字进行通信,客户端发送请求,服务器接收请求并返回响应。

AIDL(Android Interface Definition Language)是 Android 特有的一种进程间通信方式,主要用于在 Android 系统中不同应用程序之间或者同一应用程序的不同组件(如 Activity 和 Service)之间进行通信。

在 Android 开发中,当需要跨进程访问一个服务(Service)的接口时,就需要使用 AIDL。例如,一个音乐播放服务(Service),如果希望其他应用或者组件能够控制音乐的播放、暂停、音量调节等操作,就可以通过 AIDL 来定义服务的接口,其他应用或者组件可以通过绑定这个服务并调用 AIDL 定义的接口方法来实现跨进程的操作。AIDL 可以将复杂的进程间通信细节隐藏起来,开发者只需要按照规定的语法定义接口和实现服务,就可以方便地实现跨进程的功能调用。

Java 面向对象的三大特性

Java 面向对象的三大特性是封装、继承和多态。

封装是指将对象的属性和行为组合在一起,并对外部隐藏对象的内部细节。通过封装,可以提高代码的安全性和可维护性。例如,一个银行账户类,账户的余额是一个私有属性,外部不能直接访问,只能通过该类提供的存款和取款方法来操作余额。这样就防止了外部随意修改账户余额,保证了数据的安全性。在 Java 中,使用 private 关键字来限制属性的访问权限,同时提供 public 的方法来访问和修改这些属性。

继承是一种创建新类的方式,新类可以继承现有类的属性和方法。这使得代码可以复用,并且能够建立类之间的层次关系。例如,有一个动物类,它有吃和移动的方法。然后有一个猫类继承自动物类,猫类就自动拥有了吃和移动的方法,并且可以根据猫的特点重写这些方法,比如猫的移动方式是跳跃。在 Java 中,通过 extends 关键字来实现继承,子类可以继承父类的非私有成员变量和方法。

多态是指同一种行为在不同对象上具有不同的表现形式。多态分为编译时多态和运行时多态。编译时多态主要是通过方法重载实现的,运行时多态是通过方法重写和向上转型实现的。例如,有一个图形类,有计算面积的方法。然后有圆形类和矩形类继承自图形类,它们都重写了计算面积的方法。当通过图形类的引用指向圆形类或矩形类的对象时,调用计算面积的方法,会根据对象的实际类型来执行相应的计算面积方法,这就是运行时多态。

设计模式 / 工厂模式了解吗,策略模式和状态模式的各自的特点和区别

工厂模式是一种创建对象的设计模式。它的主要目的是将对象的创建和使用分离。这样做的好处是可以提高代码的可维护性和可扩展性。例如,假设有一个汽车制造工厂类,它有一个生产汽车的方法。根据传入的参数不同,这个方法可以生产不同类型的汽车,如轿车、SUV 等。在这种模式下,客户端代码只需要调用工厂类的生产方法并传入相应的参数,就可以得到所需的汽车对象,而不需要关心汽车对象是如何具体制造出来的。

策略模式是一种行为设计模式。它定义了一系列算法,将每个算法封装成一个独立的策略类,并且使这些策略可以相互替换。例如,在一个电商系统中有多种支付策略,如信用卡支付、支付宝支付、微信支付等。可以将每种支付方式封装成一个策略类,这些策略类都实现一个统一的支付接口。当用户选择支付方式时,系统只需要调用相应策略类的支付方法即可。策略模式的特点是可以灵活地切换算法,符合开闭原则,即对扩展开放,对修改封闭。

状态模式也是一种行为设计模式。它允许一个对象在其内部状态改变时改变它的行为。例如,一个订单对象有不同的状态,如未付款、已付款、已发货、已完成等。每个状态对应不同的行为,当订单状态发生变化时,订单对象的行为也会相应改变。状态模式的特点是将对象的状态和行为封装在一起,通过状态的转换来驱动行为的改变。

策略模式和状态模式的区别在于,策略模式侧重于算法的替换,不同的策略之间没有状态的转换关系。而状态模式侧重于对象状态的转换,不同状态下对象的行为不同。在策略模式中,客户端通常是主动选择策略来执行相应的行为;而在状态模式中,对象的状态转换通常是由内部的事件或者外部的操作触发的,然后根据新的状态自动执行相应的行为。

重载和重写的区别,静态方法能被重写嘛

重载是指在同一个类中,有多个方法具有相同的名字,但参数列表不同(参数的个数、类型或者顺序不同)。例如,在一个数学工具类中有一个 add 方法,可以有 add (int a, int b) 和 add (double a, double b) 这样的重载形式。重载主要是为了方便程序员使用,让一个方法名可以根据不同的参数执行不同的操作。在调用重载方法时,编译器会根据传入的实际参数来确定要调用的具体方法版本。

重写是在继承关系中,子类重新定义父类中的方法。要求方法签名(方法名、参数列表、返回类型)相同,并且访问权限不能比父类更严格。例如,父类中有一个 draw 方法,子类可以根据自己的特点重写这个 draw 方法来实现不同的绘图行为。重写的目的是实现多态,当通过父类引用指向子类对象时,调用重写后的方法会执行子类的方法版本。

静态方法不能被重写。如果在子类中定义了一个与父类静态方法签名相同的静态方法,这并不是重写,而是在子类中重新定义了一个新的静态方法。因为静态方法是属于类的,而不是属于对象的,它在类加载时就已经确定了,不会根据对象的类型而改变。

抽象类和接口区别、什么时候用它们

抽象类是一种不能被实例化的类,它可以包含抽象方法和非抽象方法。抽象方法是只有方法声明,没有方法体的方法,需要子类去实现。例如,有一个图形抽象类,它有一个抽象方法计算面积,不同的图形子类(如圆形、矩形)需要实现这个计算面积的方法。抽象类主要用于定义一组子类的通用模板,它可以包含一些子类共有的属性和方法,同时通过抽象方法来强制子类实现特定的行为。

接口是一种特殊的抽象类型,它只包含抽象方法和常量。接口中的方法默认都是抽象的,并且接口不能包含实例变量。例如,有一个可比较接口,它定义了一个 compare 方法,任何实现这个接口的类都需要实现 compare 方法来定义自己的比较规则。接口主要用于定义一种规范,多个不相关的类可以实现同一个接口来表明它们具有某种共同的行为。

在使用方面,当有一组相关的类,它们有一些共同的属性和方法,并且有一些行为需要子类去实现,同时这些类之间有明显的继承关系时,适合使用抽象类。例如,各种交通工具类可以继承自一个交通工具抽象类。当需要定义一种行为规范,让多个不相关的类都能够遵循这种规范时,适合使用接口。例如,很多不同类型的对象可能都需要实现序列化接口来方便存储和传输。

单例的两个关键字的作用

在单例模式中,有两个比较关键的关键字,一个是 private,另一个是 static。

private 关键字主要用于构造函数。将构造函数设为 private 可以防止外部类通过 new 关键字来创建单例类的多个实例。例如,在一个单例类中,通过 private 构造函数来限制实例的创建,只有在单例类内部才能调用构造函数,这样就保证了单例类的唯一性。

static 关键字主要用于单例对象的引用和获取单例对象的方法。单例对象是唯一的,它与类相关联,而不是与对象相关联。通过 static 关键字,可以在不创建类实例的情况下访问单例对象。例如,有一个静态方法来获取单例对象,这个方法可以在类的任何地方被调用,并且每次调用这个方法返回的都是同一个单例对象。这样就方便了在整个应用程序中对单例对象的访问和使用。

Android 四大组件,及其作用,说说安卓分四大组件这种模块的原因

Android 的四大组件是 Activity、Service、Broadcast Receiver 和 Content Provider。

Activity 是 Android 应用中最基本的组件,用于实现用户界面。它就像是一个屏幕,用户可以在上面进行各种交互操作,如点击按钮、输入文本等。一个 Activity 可以包含各种视图组件,如 TextView 用于显示文本,Button 用于触发操作。当用户打开一个应用时,通常首先看到的就是一个 Activity。它可以通过 Intent 进行启动和切换,比如从一个登录 Activity 切换到主页面 Activity。

Service 主要用于在后台执行长时间运行的操作,不提供用户界面。例如,音乐播放服务可以在后台持续播放音乐,即使应用的界面被切换或者设备屏幕被关闭。它可以通过 startService 或者 bindService 方法来启动,并且可以与其他组件进行通信,如通过 AIDL(Android Interface Definition Language)来实现跨进程通信。

Broadcast Receiver 用于接收系统或者应用发出的广播消息。例如,当系统的网络状态发生变化、电池电量改变或者应用自己发出一些自定义广播时,Broadcast Receiver 可以接收到这些消息并做出相应的反应。它可以通过在 AndroidManifest.xml 中注册静态广播接收器,也可以在代码中动态注册广播接收器。

Content Provider 用于在不同的应用之间共享数据。例如,一个联系人应用可以通过 Content Provider 将联系人数据提供给其他应用使用,或者一个应用可以从系统的短信 Content Provider 中读取短信内容。它通过定义数据的访问接口,其他应用可以使用 ContentResolver 来访问这些数据。

安卓分为四大组件这种模块的原因主要有以下几点。从功能分离角度看,这种划分使得每个组件可以专注于自己的功能。Activity 专注于用户界面展示和交互,Service 负责后台任务,Broadcast Receiver 处理广播消息,Content Provider 管理数据共享。这样开发者可以更容易地构建复杂的应用,并且可以根据需要灵活地组合这些组件。从代码复用角度,每个组件可以独立开发和复用。比如一个好的 Service 可以在不同的应用中用于相似的后台任务,一个 Content Provider 可以被多个应用共享数据。从系统管理角度,这种划分方便了 Android 系统对应用的管理。例如,系统可以根据组件的状态来合理地分配资源,对于处于后台的 Service 可以适当减少资源分配,而对于正在交互的 Activity 可以优先分配资源。

Activity 四种启动模式

Activity 的四种启动模式分别是 standard、singleTop、singleTask 和 singleInstance。

standard 是默认的启动模式。在这种模式下,每次启动一个 Activity,都会创建一个新的 Activity 实例。例如,假设有一个 Activity A,通过多次调用 startActivity 来启动 A,就会创建多个 A 的实例,并且这些实例会被依次放入任务栈(Task Stack)中。任务栈是用于管理 Activity 的栈结构,新启动的 Activity 会被压入栈顶。

singleTop 模式下,如果要启动的 Activity 已经位于任务栈的栈顶,就不会再创建新的实例,而是直接调用该 Activity 的 onNewIntent 方法。例如,有一个 Activity B,当它处于栈顶时,再次启动 B,系统会复用这个栈顶的 B 实例,将新的 Intent 传递给它,通过 onNewIntent 方法可以处理这个新的 Intent。这种模式适用于一些接收通知并更新界面的场景,比如新闻类应用收到新的新闻推送时,打开新闻详情 Activity,如果这个 Activity 已经在栈顶,就直接更新内容。

singleTask 模式下,在整个任务栈中,只会有一个该 Activity 的实例。如果要启动的 Activity 已经存在于任务栈中,系统会将这个 Activity 之上的其他 Activity 全部清除,然后将该 Activity 置于栈顶。例如,有一个主页面 Activity C 和一个登录 Activity D,当从其他应用启动 C(C 为 singleTask 模式),如果 C 已经在任务栈中,系统会清除 C 之上的其他 Activity,然后将 C 置于栈顶,这样可以保证每次回到 C 时,它都是独立的、完整的主页面状态。

singleInstance 模式比较特殊,在这种模式下,该 Activity 会独占一个任务栈。当启动这个 Activity 时,系统会为它创建一个新的任务栈,并且这个 Activity 在这个任务栈中是唯一的。例如,有一个系统设置 Activity E 是 singleInstance 模式,当从应用中启动 E 时,E 会在自己的任务栈中,与应用的其他 Activity 所在的任务栈分开。这种模式适用于一些需要独立运行并且不希望被其他 Activity 干扰的场景,比如某些系统级别的特殊设置 Activity。

onSaveInstanceState 作用

onSaveInstanceState 方法主要用于在 Activity 可能被销毁之前保存当前的状态信息。

当系统配置发生变化(如屏幕旋转)或者系统内存不足需要回收 Activity 时,Activity 有可能会被销毁。在这种情况下,onSaveInstanceState 方法会被调用。例如,当用户旋转手机屏幕时,当前的 Activity 会重新创建,但是用户可能不希望之前在 Activity 中的输入信息或者界面状态丢失。通过 onSaveInstanceState 方法,可以将这些重要的信息保存到一个 Bundle 对象中。Bundle 是一个可以存储各种数据类型(如字符串、整数、布尔值等)的容器。

假设在一个编辑文本的 Activity 中有一个 EditText,我们可以在 onSaveInstanceState 方法中保存 EditText 中的文本内容。当 Activity 重新创建时,可以在 onCreate 方法中从 Bundle 中取出之前保存的文本内容,然后将其恢复到 EditText 中。这样就保证了用户体验的连贯性,用户不需要重新输入之前的内容。

另外,onSaveInstanceState 方法并不是在所有 Activity 销毁的情况下都会被调用。只有当 Activity 是被系统异常销毁(如系统内存不足或者配置改变)时才会调用。如果是开发者自己调用 finish 方法来销毁 Activity,onSaveInstanceState 方法通常不会被调用。并且,这个方法的调用时机是在 onStop 方法之前,因为在 onStop 之后 Activity 可能已经被完全销毁,没有机会保存状态信息了。

用过什么 layout,介绍一下它们的区别

在 Android 开发中,常用的布局(layout)有 LinearLayout、RelativeLayout、FrameLayout、TableLayout 和 ConstraintLayout。

LinearLayout 是线性布局,它可以按照水平或者垂直方向排列子视图。例如,在一个垂直方向的 LinearLayout 中,子视图会从上到下依次排列。它的优点是布局简单直观,适合创建一些简单的界面,如列表型的布局。它有两个重要的属性,一个是 orientation,用于指定排列方向(水平或垂直),另一个是 layout_weight,用于按照比例分配子视图在布局中的空间。比如,有两个按钮在一个水平 LinearLayout 中,设置它们的 layout_weight 可以让它们按照不同的比例分配水平空间。

RelativeLayout 是相对布局,它通过子视图之间的相对位置关系来进行布局。子视图可以相对于父视图或者其他子视图来定位。例如,一个按钮可以位于另一个按钮的下方或者右侧。这种布局灵活性较高,适合创建复杂的界面,尤其是那些需要精确控制视图位置的情况。它通过一系列属性来定义相对位置,如 layout_above、layout_below、layout_toLeftOf 等。不过,由于相对位置的复杂性,当布局变得非常复杂时,可能会导致布局文件难以理解和维护。

FrameLayout 是帧布局,它所有的子视图都会堆叠在左上角。后添加的子视图会覆盖前面的子视图。这种布局简单,但是功能有限。它通常用于一些简单的场景,如需要在一个位置显示不同的视图,或者作为其他布局的容器来暂时隐藏一些视图。例如,在一个图片查看应用中,可以使用 FrameLayout 来显示图片和一个加载进度条,当图片加载时,进度条显示在图片之上,当图片加载完成后,进度条可以隐藏。

TableLayout 是表格布局,它以表格的形式排列子视图。它由行(TableRow)和列组成,每个 TableRow 代表一行,行中的子视图代表列。这种布局适合展示数据表格类型的内容,如显示学生成绩表。它可以通过设置列的宽度等属性来调整表格的外观,并且可以根据内容自动调整行高。

ConstraintLayout 是一种相对较新的布局,它通过约束(constraint)来定义子视图的位置。它结合了 RelativeLayout 的灵活性和 LinearLayout 的简单性。可以通过添加约束来让子视图相对于其他视图、父视图或者布局边界来定位。例如,一个按钮可以约束在距离父视图顶部一定距离并且距离另一个按钮一定距离的位置。这种布局在构建复杂的自适应界面时非常有用,并且在 Android Studio 的布局编辑器中有很好的可视化支持,方便开发者快速设计布局。

ListView 和 RecyclerView 区别

ListView 是 Android 早期用于展示大量数据列表的视图。它的工作方式是基于 Adapter 模式,Adapter 负责提供数据并创建每个列表项的视图。当 ListView 需要显示一个新的项目时,Adapter 会创建对应的视图。它的优点是简单易用,适合简单的数据列表展示。例如,展示一个联系人列表,每个联系人信息作为一个列表项。

然而,ListView 存在一些局限性。它在处理复杂的列表项布局变化和大量数据更新时性能不够理想。因为它的视图复用机制相对简单,对于复杂的列表项,如包含多种不同类型子视图的列表项,处理起来比较麻烦。

RecyclerView 是在 ListView 基础上改进的视图,用于更高效地展示列表数据。它具有更灵活的布局管理器,包括线性布局、网格布局和瀑布流布局等多种形式。例如,在一个电商应用中,可以使用线性布局的 RecyclerView 展示商品列表,也可以使用网格布局展示商品分类。

RecyclerView 的视图复用机制更加高效。它通过 ViewHolder 模式,能够更好地处理列表项视图的复用,减少了不必要的视图创建和销毁,从而提高了性能。对于数据更新,RecyclerView 提供了更精细的控制,可以局部刷新数据,比如只更新列表中的某一个项目,而不是像 ListView 那样经常需要全部重新加载。

在动画支持方面,RecyclerView 也有更好的表现。它能够方便地添加各种动画效果,如列表项的添加、删除和移动动画,增强了用户体验。例如,在一个任务管理应用中,当用户完成一个任务并将其从列表中删除时,RecyclerView 可以通过动画平滑地展示任务项的消失过程。

ViewPager 缓存 / 预加载

ViewPager 是用于实现页面切换效果的视图容器,它可以左右滑动来展示不同的页面,就像一个卡片式的布局。

在缓存方面,ViewPager 默认会缓存当前页面的左右相邻页面。例如,当有三个页面 A、B、C 时,在展示页面 B 时,ViewPager 会提前加载并缓存页面 A 和 C。这种缓存机制是为了让页面切换更加流畅,减少用户等待时间。当用户从页面 B 切换到页面 C 时,由于 C 已经被缓存,所以可以快速地显示出来。

预加载是和缓存相关的一个重要特性。它可以通过设置 ViewPager 的预加载页面数量来控制。除了默认缓存相邻页面外,还可以设置预加载更多的页面。预加载的原理是在当前页面被显示时,根据设置的预加载策略,提前加载后续的页面。这对于一些数据加载比较复杂的页面非常有用,比如页面中包含网络图片或者大量的数据计算。

预加载的数量设置需要权衡性能和用户体验。如果预加载过多的页面,可能会占用过多的内存和网络资源,导致应用性能下降。如果预加载太少,又可能会出现页面切换时的延迟,影响用户体验。例如,在一个图片浏览应用中,每个页面展示一张高清图片,如果预加载太多图片,可能会导致内存占用过高,但是预加载太少,用户切换图片时可能需要等待图片加载。

Android 广播的类型

在 Android 中,广播主要分为两种类型,即标准广播和有序广播。

标准广播是一种完全异步的广播。当一个标准广播被发送出去后,所有注册接收该广播的广播接收器几乎会同时接收到这个广播。例如,当系统发出一个电池电量变化的标准广播时,所有关注电池电量的应用中的广播接收器会同时开始处理这个广播。这种广播的优点是效率高,广播发送者不需要等待广播接收器的处理结果,可以快速地将广播发送出去。但是它的缺点是无法控制广播的接收顺序,也不能中断广播的传递。

有序广播则是一种同步广播。当一个有序广播被发送时,广播会按照优先级顺序依次传递给各个广播接收器。在 AndroidManifest.xml 文件中可以通过设置广播接收器的优先级来确定接收顺序。例如,系统在启动完成后会发送一个有序广播,高优先级的广播接收器可以首先接收到这个广播,并且可以对广播进行处理,甚至可以截断广播,使得后面优先级较低的广播接收器无法接收到这个广播。这种广播的优点是可以控制广播的接收顺序和处理流程,适用于一些需要按照特定顺序处理广播的场景。

另外,从广播的发送者角度,还可以分为系统广播和自定义广播。系统广播是由 Android 系统发出的广播,用于通知应用系统状态的变化,如网络连接变化、屏幕亮灭等。自定义广播是由应用自己发送的广播,用于在应用内部或者不同应用之间进行消息传递。例如,一个应用的某个模块完成了一个任务,可以发送一个自定义广播来通知其他模块进行相应的操作。

Intent 传递数据

Intent 是 Android 中用于组件间通信的重要机制,它可以有效地传递数据。

在 Activity 之间传递数据时,可以通过 Intent 的 putExtra 方法。例如,有一个登录 Activity 和一个主页面 Activity,在登录成功后,可以将用户的信息(如用户名)通过 Intent 传递给主页面 Activity。在登录 Activity 中,可以这样操作:

“Intent intent = new Intent(LoginActivity.this, MainActivity.class); intent.putExtra("username", "John"); startActivity(intent);”

在接收数据的 MainActivity 中,可以在 onCreate 方法中获取数据:

“String username = getIntent().getStringExtra("username");”

除了基本的数据类型,Intent 还可以传递复杂的数据结构,如 Bundle。Bundle 是一个可以存储多种数据类型的容器。如果要传递一个包含多个数据的对象,可以将对象的各个属性放入 Bundle 中,然后通过 Intent 传递 Bundle。例如,传递一个包含用户姓名、年龄和地址的用户对象,可以这样操作:

“Bundle bundle = new Bundle(); bundle.putString("name", user.getName()); bundle.putInt("age", user.getAge()); bundle.putString("address", user.getAddress()); intent.putExtra("userInfo", bundle);”

在接收方,可以通过类似的方式从 Bundle 中获取数据。

Intent 还可以用于启动 Service 或者发送广播,并且在这个过程中也可以传递数据。例如,在启动一个音乐播放服务时,可以通过 Intent 传递音乐的资源路径、播放模式等信息。在发送广播时,可以通过 Intent 传递广播的参数,让接收广播的广播接收器根据这些参数进行相应的操作。

生命周期,两个 Activity 启动的回调,如果把一个应用进程直接 kill 掉,生命周期会怎么走

Activity 的生命周期包含多个阶段和回调方法。

当一个 Activity 启动时,首先会调用 onCreate 方法。在这个方法中,可以进行一些初始化的操作,如设置布局、初始化变量等。例如,加载布局文件可以通过 “setContentView (R.layout.activity_main);” 这样的操作在 onCreate 方法中完成。

接着会调用 onStart 方法,此时 Activity 已经可见,但还没有获取焦点。比如在这个阶段可以进行一些动画的初始化,或者开始一些和视图可见性相关的操作。

然后是 onResume 方法,在这个阶段 Activity 获取了焦点,用户可以进行交互操作。例如,当一个游戏 Activity 进入 onResume 阶段,就可以开始接收用户的触摸操作来控制游戏。

当启动第二个 Activity 时,第一个 Activity 会经历 onPause 方法。在这个方法中,可以暂停一些正在进行的操作,如暂停动画、暂停音频播放等。

然后第一个 Activity 会进入 onStop 方法,此时第一个 Activity 不再可见。不过它仍然存在于内存中,在一定条件下可以重新回到前台。

对于第二个 Activity,它会依次经历 onCreate、onStart 和 onResume 方法,和第一个 Activity 启动时类似。

如果把一个应用进程直接 kill 掉,所有 Activity 的生命周期会立即结束。不会再按照正常的生命周期回调顺序走。这些 Activity 所占用的内存资源会被系统回收,并且没有机会执行保存数据等操作。如果希望在进程被 kill 之前保存一些重要的数据,可以通过 onSaveInstanceState 方法,将数据存储在 Bundle 中,当 Activity 重新创建时可以尝试恢复这些数据。但是如果是直接 kill 进程,这种保存的数据恢复机制也无法起作用。

常用的多线程工具类在 Android 中的应用(如 Handler、AsyncTask 等),Android 多线程实现方式

在 Android 中,多线程是非常重要的,因为它可以避免主线程(UI 线程)阻塞,提升用户体验。

Handler 是 Android 中用于在不同线程之间传递消息的工具类。它主要用于在子线程中更新 UI。例如,在一个网络请求的子线程中,当获取到数据后,不能直接在子线程更新 UI,需要通过 Handler 发送消息给主线程,然后在主线程的 Handler 的处理方法(handleMessage)中更新 UI。这样就保证了 UI 操作的安全性。

AsyncTask 是一个抽象类,它简化了在后台线程执行任务并在主线程更新结果的过程。它有几个重要的方法,如 doInBackground 在后台线程执行任务,onProgressUpdate 在主线程更新进度,onPostBackground 在主线程更新最终结果。比如,在下载文件时,doInBackground 方法可以执行文件下载的具体操作,在下载过程中通过 publishProgress 方法来更新进度,然后 onProgressUpdate 方法在主线程接收进度更新并显示给用户,下载完成后,onPostBackground 方法可以处理下载后的文件。

Android 多线程的实现方式还有很多。可以通过继承 Thread 类,重写 run 方法来定义线程的任务。例如,创建一个新的线程类来执行长时间的计算任务。也可以通过实现 Runnable 接口,将实现 Runnable 的对象作为参数传入 Thread 的构造函数来启动线程。另外,还可以使用线程池(ThreadPoolExecutor)来管理多个线程,提高线程的复用率,比如在处理多个网络请求时,通过线程池来分配线程,避免频繁创建和销毁线程带来的开销。

说说 Handler 机制

Handler 机制是 Android 中用于线程间通信的重要机制。

Handler 主要包含四个重要元素:Handler、Message、MessageQueue 和 Looper。Handler 用于发送和处理消息。可以通过 Handler 的 sendMessage 方法发送消息。消息(Message)是线程间传递的载体,它可以携带数据,如通过 Message 的 obj 属性或者通过 Bundle 来携带复杂的数据结构。

MessageQueue 是一个消息队列,它存储了通过 Handler 发送的消息,按照消息的发送时间等顺序排队。这个队列是一个先进先出的结构,用于保证消息的有序处理。例如,在一个复杂的多任务场景中,多个子线程通过 Handler 发送消息给主线程,这些消息会在 MessageQueue 中排队等待处理。

Looper 是一个循环器,它会不断地从 MessageQueue 中取出消息,并将消息分发给对应的 Handler 进行处理。每个线程只有一个 Looper,它在该线程的生命周期内一直循环运行。在主线程中,Android 系统会自动创建 Looper,而在子线程中如果要使用 Handler,需要先创建 Looper。例如,在一个自定义的子线程中,需要通过 Looper.prepare 和 Looper.loop 方法来开启和运行 Looper,这样才能使该线程中的 Handler 正常工作。

当一个 Handler 发送消息时,消息会被添加到与该 Handler 关联的 MessageQueue 中。然后 Looper 会在适当的时候从 MessageQueue 中取出消息,根据消息中携带的 Handler 引用,将消息分发给对应的 Handler 进行处理。这种机制使得不同线程之间可以安全、有序地进行通信。

Handler 引起的内存泄露,当 messagequeue 空的时候,Handler 怎么进行阻塞的,Handler 原理,Handler 是如何实现延时的

Handler 引起内存泄漏主要是因为 Handler 通常是内部类,它会持有外部类的引用。如果 Handler 的生命周期比外部类长,并且在 MessageQueue 中有未处理的消息,这些消息会持有 Handler 的引用,从而导致外部类无法被垃圾回收。例如,在一个 Activity 中定义了一个 Handler,当 Activity 要被销毁时,如果 Handler 还有未处理的消息,那么 Activity 就无法被正确回收,导致内存泄漏。

当 MessageQueue 为空时,Looper 会阻塞。Looper 的 loop 方法中有一个循环,它会不断地从 MessageQueue 中取消息。当 MessageQueue 为空时,它会进入阻塞状态,等待有消息进入 MessageQueue。这是通过底层的 Linux 管道机制实现的。这种阻塞机制可以避免 Looper 在没有消息处理时浪费 CPU 资源。

Handler 的原理是基于前面提到的 Handler、Message、MessageQueue 和 Looper 的协同工作。Handler 用于发送和接收消息,Message 是消息的载体,MessageQueue 存储消息,Looper 循环取出消息并分发。它们之间紧密配合,使得在不同线程之间的通信能够顺利进行。

Handler 实现延时主要是通过在发送消息时设置消息的延迟时间。在 sendMessageDelayed 方法或者 postDelayed 方法中,可以指定一个延迟时间。当发送这样的延迟消息时,MessageQueue 会根据消息的发送时间(包括延迟时间)来排序消息。当 Looper 循环取消息时,会检查消息的发送时间,只有当时间到达后,才会将消息分发给对应的 Handler 进行处理。例如,在一个定时提醒的应用中,可以通过 Handler 发送一个延迟消息,当延迟时间到达后,在 Handler 的处理方法中弹出提醒通知。

内存泄漏检测方案,常见内存泄漏有哪些

内存泄漏检测方案有多种。

一种是使用 Android Studio 自带的内存分析工具,如 LeakCanary。LeakCanary 是一个开源的内存泄漏检测库,它可以自动检测应用中的内存泄漏情况。当有内存泄漏发生时,它会生成详细的泄漏报告,包括泄漏的对象、引用链等信息。它的工作原理是通过监控对象的生命周期,当一个对象应该被垃圾回收但却没有被回收时,它就会发出警报。

另一种方法是通过分析 Android 系统的日志。可以在代码中添加一些日志输出,当怀疑有内存泄漏时,观察内存的使用情况。例如,在对象的创建和销毁处添加日志,记录对象的数量和内存占用情况。

常见的内存泄漏有以下几种。

一是单例模式引起的内存泄漏。如果单例对象持有一个 Activity 或者其他生命周期较短的对象的引用,当这个对象应该被回收时,由于单例对象的生命周期很长,导致该对象无法被回收。例如,一个单例的工具类中持有一个 Activity 的引用,用于在工具类中更新 Activity 的 UI,当 Activity 要被销毁时,由于单例的存在,Activity 无法被正确回收。

二是静态变量引起的内存泄漏。如果一个静态变量引用了一个生命周期较短的对象,同样会导致这个对象无法被回收。例如,在一个类中有一个静态变量,它引用了一个 Activity 的视图,当 Activity 被销毁后,由于静态变量的存在,视图无法被垃圾回收。

三是未取消的注册引起的内存泄漏。例如,在 Activity 中注册了一个广播接收器或者传感器监听器,但是在 Activity 被销毁时没有取消注册,这些注册对象会一直持有 Activity 的引用,导致 Activity 无法被回收。

View 的 60ms 刷新机制了解,怎么检测 View 卡顿

Android 系统为了保证 UI 的流畅性,有一个 60ms 刷新机制。这是因为人眼对画面的感知是有一定限度的,当画面的刷新频率达到每秒 60 帧左右时,人眼会感觉画面是流畅的。在 Android 中,1000ms 除以 60 约等于 16.67ms,这意味着每一帧的绘制时间最好不要超过 16.67ms,这样才能保证 UI 的流畅性。

为了实现这个目标,Android 系统会定期发送一个垂直同步信号(VSync),这个信号的周期大约是 16.67ms。当 View 需要刷新时,会在收到 VSync 信号后开始绘制操作。整个绘制过程包括测量(measure)、布局(layout)和绘制(draw)三个阶段。

检测 View 卡顿可以通过多种方法。

一种是使用 Android 系统提供的 Systrace 工具。Systrace 可以记录系统的各种操作,包括 CPU 的使用、线程的调度以及 View 的绘制等。通过分析 Systrace 生成的报告,可以发现 View 在哪个阶段出现了卡顿,比如是在测量阶段花费时间过长,还是在绘制阶段有问题。

另一种方法是在代码中添加性能检测点。例如,在 View 的绘制方法的开始和结束处记录时间,计算绘制过程的时间。如果这个时间超过了 16.67ms,就可以判断可能出现了卡顿。还可以使用一些第三方的性能检测库,它们可以更方便地监控 View 的性能,并且提供详细的卡顿分析报告。

ANR 如何定位和监控

ANR(Application Not Responding)是指应用无响应的情况。当 Android 系统检测到应用在一段时间内没有响应输入事件或者广播接收器在规定时间内没有执行完操作,就会判定为 ANR。

定位 ANR 问题,首先可以查看系统的日志文件。在发生 ANR 后,系统会在 /data/anr/traces.txt 文件中记录相关的栈信息。这些信息包含了当时正在执行的线程以及它们的调用栈。通过分析这些栈信息,可以找出可能导致 ANR 的原因。例如,如果主线程(UI 线程)被一个长时间运行的操作(如复杂的数据库查询或者网络请求)阻塞,就会在日志中看到主线程卡在这些操作上。

另外,也可以使用性能分析工具来监控应用,预防 ANR 的发生。像 Android Studio 中的 Profiler 工具,它可以实时监测应用的 CPU、内存、网络等资源的使用情况。在测试过程中,观察主线程的执行情况,如果发现主线程长时间处于高负载或者阻塞状态,就有可能引发 ANR。还可以通过设置自定义的监控点来检测关键代码段的执行时间。例如,对于一些可能会影响 UI 响应的重要操作(如加载大量数据到列表视图),记录其开始和结束时间,若超过一定阈值,就进行预警。

从代码层面来看,要避免在主线程进行耗时操作。如果必须要进行耗时操作,比如网络请求或文件读取,可以使用异步任务(如 AsyncTask)或者线程池将这些任务放到后台线程执行。并且,对于广播接收器,要确保在规定时间(如前台广播 10 秒,后台广播 60 秒)内完成操作,否则很容易引发 ANR。

View 绘制流程 /requestLayout/invalidate,measure、layout、draw 流程,滑动冲突

View 的绘制流程主要包括 measure、layout 和 draw 三个阶段。

Measure 阶段主要是确定 View 的大小。当父视图(ViewGroup)需要确定子视图的大小时,会调用子视图的 measure 方法。子视图会根据自身的布局参数(如宽高的模式是固定值、wrap_content 还是 match_parent)和父视图提供的约束条件来计算自己的大小。例如,一个 TextView 如果设置为 wrap_content,它会根据文本内容的长度来确定自己的宽度,同时根据字体大小等因素确定高度。这个过程是递归的,从根视图开始,依次向下传递测量要求,直到所有的视图都完成测量。

Layout 阶段是确定 View 的位置。在 measure 阶段确定了大小之后,layout 阶段会根据父视图的位置和子视图自身的大小来确定子视图的位置。父视图会调用子视图的 layout 方法,传递给子视图它在父视图中的位置参数(如左上角的坐标)。例如,在一个线性布局(LinearLayout)中,子视图会根据线性布局的方向(水平或垂直)和自己的大小依次排列在相应的位置。

Draw 阶段是将 View 绘制到屏幕上。在这个阶段,View 会根据自身的状态(如颜色、背景等)和内容(如文本、图像)进行绘制。它会先绘制背景,然后绘制内容,最后绘制装饰(如边框等)。例如,一个按钮(Button)会先绘制背景颜色,然后绘制按钮上的文本,最后可能会绘制一些按下效果的边框。

requestLayout 方法主要用于当视图的大小或者位置需要重新计算时。当调用这个方法,会触发整个视图树(从当前视图向上直到根视图)的 measure 和 layout 过程。例如,当一个视图的内容发生变化,导致其大小可能改变时,就需要调用 requestLayout 来重新布局。

invalidate 方法主要用于请求重绘视图。当视图的外观(如颜色、文本内容)发生变化时,调用 invalidate 会触发 draw 过程。例如,当一个 TextView 的文本内容被更新后,调用 invalidate 会使视图重新绘制,将新的文本显示出来。

滑动冲突是在嵌套的视图结构中,当内外层视图都可以滑动时可能出现的问题。例如,在一个可滑动的列表视图(ListView)中嵌套了一个可水平滑动的视图(如 HorizontalScrollView)。解决滑动冲突的方法有多种。一种是外部拦截法,通过重写父视图(ViewGroup)的 onInterceptTouchEvent 方法,根据触摸事件的位置和滑动方向等来判断是否拦截触摸事件。另一种是内部拦截法,在子视图中通过 requestDisallowInterceptTouchEvent 方法来请求父视图不拦截触摸事件,同时在父视图中适当处理这种请求。

事件分发,讲完流程之后问的比较深入。如何让事件进行双向传递,比如子 View 已经消费了的事件怎么传回到 VG 去

在 Android 中,事件分发主要涉及三个方法:dispatchTouchEvent、onInterceptTouchEvent 和 onTouchEvent。

dispatchTouchEvent 方法用于分发触摸事件,它是事件分发的起点。当一个触摸事件发生时,会从最顶层的视图(一般是 Activity 的根视图)开始,通过这个方法将事件向下传递。例如,在一个布局结构中,触摸事件首先会到达根视图,然后根视图会通过 dispatchTouchEvent 方法将事件传递给它的子视图。

onInterceptTouchEvent 方法主要用于父视图(ViewGroup)拦截触摸事件。当父视图想要拦截子视图的触摸事件时,可以在这个方法中返回 true。例如,在一个自定义的 ViewGroup 中,如果希望在某些情况下阻止子视图接收触摸事件,就可以在这个方法中进行判断并返回 true 来拦截。

onTouchEvent 方法用于处理触摸事件。当一个视图接收到触摸事件并且没有被父视图拦截时,就会通过这个方法来处理事件。例如,一个按钮(Button)的点击事件就是通过 onTouchEvent 方法来处理的。

如果子视图已经消费了事件,要将事件传回到 ViewGroup,这是一个比较复杂的情况。一种方法是通过自定义的事件传递机制。例如,可以在子视图中定义一个接口,当子视图消费事件后,通过这个接口回调将事件信息传递给父视图。或者在子视图中设置一个标记,当事件处理完成后,通过某种方式(如发送广播或者调用父视图的公共方法)告知父视图事件已经处理完毕。另外,也可以在父视图的 onInterceptTouchEvent 方法中进行特殊的逻辑处理。比如,当子视图处理完事件后,父视图可以根据子视图的状态或者其他条件来判断是否需要重新获取事件控制权,通过在这个方法中动态地调整返回值(true 或 false)来实现事件的双向传递。

介绍一下 Binder 和原理

Binder 是 Android 系统中一种用于进程间通信(IPC)的机制。

它的主要作用是让不同进程的组件能够相互通信。在 Android 中,由于系统的安全性和稳定性要求,每个应用都运行在自己独立的进程中,但是很多时候应用之间或者应用内部的不同组件之间需要交换信息或者共享资源,这就需要 Binder 来实现。

从原理上来说,Binder 采用了 C/S(客户 / 服务器)架构。在这个架构中,有服务端(Server)和客户端(Client)。服务端提供服务,它会通过 Binder 驱动在内核空间创建一个 Binder 实体,这个实体有一个唯一的引用(句柄)。客户端如果想要使用服务端的服务,会通过这个引用向服务端发送请求。

当客户端发起请求时,请求会首先通过系统的用户空间到达 Binder 驱动。Binder 驱动会对请求进行处理,它会根据请求中的目标(服务端的 Binder 实体)来将请求传递给服务端。服务端接收到请求后,会执行相应的操作,然后将结果通过 Binder 驱动返回给客户端。

Binder 驱动在这个过程中起到了关键的作用。它负责管理 Binder 实体和引用,确保请求的正确传递和安全。例如,它会检查客户端是否有访问服务端服务的权限,防止非法的访问。同时,它还可以对通信进行优化,比如采用共享内存的方式来提高通信效率。因为在 Android 系统中,不同进程的内存空间是相互独立的,通过 Binder 驱动可以在一定程度上实现内存的共享,减少数据的复制和传输开销。

AIDL 了解过吗

AIDL(Android Interface Definition Language)是 Android 中用于实现跨进程通信的一种语言。

它主要用于定义服务(Service)的接口,使得不同进程的客户端能够访问服务端提供的服务。例如,在一个音乐播放应用中,音乐播放服务运行在一个独立的进程中,而控制音乐播放的 Activity 运行在另一个进程中。通过 AIDL 可以定义音乐播放服务的接口,如播放、暂停、调节音量等操作,然后 Activity 可以通过绑定这个服务并调用 AIDL 定义的接口方法来实现跨进程控制音乐播放。

AIDL 的语法类似于 Java 接口的定义。它可以定义方法的参数和返回值类型,这些类型必须是支持跨进程传递的类型,如基本数据类型、String、CharSequence 以及实现了 Parcelable 接口的自定义对象。当定义好 AIDL 文件后,Android 开发工具会自动生成对应的 Java 接口和实现类,用于在服务端和客户端之间进行通信。

在服务端,需要实现 AIDL 定义的接口,将接口方法与实际的服务操作(如音乐播放的具体实现)相对应。在客户端,需要通过 bindService 方法绑定服务,获取服务的代理对象,这个代理对象实现了 AIDL 定义的接口,客户端可以通过这个代理对象调用服务端的方法,就好像在同一个进程中一样。整个过程中,AIDL 隐藏了复杂的跨进程通信细节,通过 Binder 机制来实现服务端和客户端之间的高效通信。

二叉树有哪些遍历方式,二叉树的层次遍历,从最下面一层出发

二叉树主要有三种遍历方式:前序遍历、中序遍历和后序遍历。

前序遍历的顺序是根节点、左子树、右子树。这种遍历方式先访问根节点,然后递归地遍历左子树,最后遍历右子树。例如,对于一个简单的二叉树,根节点为 A,左子节点为 B,右子节点为 C,前序遍历的结果就是 A、B、C。它的应用场景包括复制二叉树,在复制时先复制根节点,再复制左右子树。

中序遍历的顺序是左子树、根节点、右子树。以刚才的二叉树为例,中序遍历的结果是 B、A、C。中序遍历对于二叉搜索树来说,可以按照从小到大的顺序输出节点的值。例如,二叉搜索树的中序遍历结果就是有序的节点值序列,这在排序和查找相关操作中有重要应用。

后序遍历的顺序是左子树、右子树、根节点。对于上述二叉树,后序遍历的结果是 B、C、A。后序遍历常用于计算二叉树的一些后序相关属性,比如计算二叉树的高度,需要先计算左右子树的高度,最后才能确定整棵树的高度。

二叉树的层次遍历是按照树的层次,从根节点开始,一层一层地访问节点。如果要从最下面一层出发进行层次遍历,可以使用队列来辅助实现。首先将最底层的节点依次放入队列,然后每次从队列中取出一个节点,访问该节点,并将其上层节点(如果有)放入队列。例如,对于一个三层的二叉树,最底层有四个节点,先将这四个节点放入队列,然后取出一个节点,访问它,并将它的父节点放入队列,如此循环,就可以实现从最下面一层出发的层次遍历。这种遍历方式在处理一些需要按照层次逆序处理的问题时很有用,比如在一些图形渲染中,从底层开始渲染可以避免上层节点对下层节点的遮挡计算错误。

数组链表的区别

数组和链表是两种不同的数据结构。

数组是一种连续存储的数据结构。它在内存中占用一段连续的空间,元素之间的存储位置是相邻的。例如,一个整数数组 int [] arr = {1, 2, 3, 4, 5},在内存中,这些整数是依次紧挨着存储的。

数组的优点在于可以通过索引快速访问元素。访问数组元素的时间复杂度是 O (1)。例如,要访问 arr [3],可以直接定位到内存中的相应位置获取元素,非常高效。但是数组的大小是固定的,在创建数组时就需要指定大小。如果要增加或减少数组的容量,需要重新分配内存空间,这是比较复杂的操作。

链表则是由节点组成的数据结构,每个节点包含数据和指向下一个节点的引用(在单向链表中)。例如,一个简单的链表节点结构可以包含一个数据域和一个 next 指针。链表的内存空间不需要连续,节点可以分散在内存的不同位置。

链表的优点是插入和删除操作相对灵活。在链表中插入或删除一个节点,只需要改变相关节点的引用即可。例如,要在两个节点 A 和 B 之间插入一个节点 C,只需要将 A 的 next 指针指向 C,再将 C 的 next 指针指向 B。但是链表的缺点是访问元素效率较低,因为要访问链表中的某个节点,需要从链表头开始逐个遍历节点,时间复杂度是 O (n)。

说说 HashMap 结构

HashMap 是 Java 中的一种重要的数据结构,用于存储键值对。

它的内部结构主要包括数组、链表(在 Java 8 之后还有红黑树)。首先,HashMap 有一个数组,这个数组用于存储键值对的位置信息。数组的每个元素称为桶(bucket)。当要存储一个键值对时,会根据键的哈希值计算出它在数组中的位置,也就是桶的位置。

如果不同的键计算出的哈希值相同,导致要存储到同一个桶中,就会在这个桶上形成一个链表(在 Java 8 之前一直是链表结构)。例如,有两个键值对(key1, value1)和(key2, value2),它们的哈希值相同,就会在对应的桶位置形成一个链表,先存储的键值对在链表头部,后存储的在链表尾部。

在 Java 8 之后,当链表的长度达到一定阈值(默认为 8)时,链表会转换为红黑树。红黑树是一种自平衡的二叉搜索树,它可以提高查找效率。当桶中的元素以红黑树的形式存储时,查找一个键值对的时间复杂度可以从链表的 O (n) 降低到 O (log n)。

HashMap 还维护了一些重要的属性,如容量(capacity)、加载因子(load factor)。容量是数组的大小,加载因子用于决定何时对 HashMap 进行扩容。当存储的键值对数量超过容量乘以加载因子时,HashMap 会进行扩容操作,扩容后的容量一般是原来的两倍。

HashMap key 的存储位置怎么计算

在 HashMap 中,key 的存储位置是通过对 key 的哈希值进行计算得到的。

首先,会调用 key 对象的 hashCode 方法来获取它的哈希值。这个哈希值是一个整数。例如,对于一个 String 类型的 key,它的 hashCode 方法会根据字符串的内容计算出一个哈希值。不同的 key 对象可能会计算出相同的哈希值,这称为哈希冲突。

得到哈希值后,会通过一个位运算来计算它在数组(桶)中的位置。具体的计算方式是将哈希值与数组长度减 1 进行按位与操作(hash & (length - 1))。这样做的目的是保证计算出的位置在数组的范围内。例如,当数组长度为 16 时,长度减 1 的二进制表示是 1111,通过与哈希值进行按位与操作,可以将哈希值映射到 0 到 15 之间的一个位置。

如果发生哈希冲突,即不同的 key 计算出相同的存储位置,就会在这个位置形成链表或者红黑树(在 Java 8 之后)来存储多个键值对。这种通过哈希值计算存储位置的方式可以使 HashMap 在大多数情况下能够快速地存储和查找键值对。

HashMap,Hashtable,ConcurrentHashMap 区别

HashMap、Hashtable 和 ConcurrentHashMap 都是用于存储键值对的数据结构,但它们有很多区别。

HashMap 是最常用的,它不是线程安全的。这意味着在多线程环境下,如果多个线程同时对 HashMap 进行操作,可能会导致数据不一致或者其他异常情况。例如,一个线程正在插入一个键值对,另一个线程同时在读取或者修改 HashMap,可能会出现读取到错误的数据或者插入失败等问题。HashMap 允许键和值为 null,这在某些情况下可以方便编程,比如可以将 null 作为一个特殊的值来表示不存在或者未初始化。

Hashtable 是早期的键值对存储结构,它是线程安全的。它的线程安全是通过在每个方法上使用 synchronized 关键字来实现的。这使得在多线程环境下,每次只能有一个线程访问 Hashtable 的方法。但是这种方式也导致了性能的损失,因为在高并发场景下,频繁的锁竞争会使效率降低。Hashtable 不允许键和值为 null,这是为了避免在多线程环境下因为 null 值而引起的歧义或者错误。

ConcurrentHashMap 是为了解决高并发场景下的线程安全和性能问题而设计的。在 Java 8 之前,它采用了分段锁(Segment)的机制,将整个数据结构分成多个段,每个段有自己独立的锁。这样,不同段的操作可以并发进行,只有在对同一个段进行操作时才会产生锁竞争。在 Java 8 之后,它采用了更精细的 CAS(Compare - and - Swap)操作和 synchronized 关键字相结合的方式,在保证线程安全的同时,进一步提高了性能。ConcurrentHashMap 不允许键为 null,但是值可以为 null。它在高并发的场景下,如多线程的缓存系统或者分布式系统中的数据存储,能够高效地进行数据的存储和读取。

说说 ConcurrentHashMap 分段锁详解

ConcurrentHashMap 在 Java 8 之前主要采用分段锁的机制来实现高并发下的线程安全。

它将整个哈希表分成多个段(Segment),每个段在逻辑上类似于一个独立的小哈希表。每个段都有自己的锁,这就意味着不同段之间的操作可以并发进行。例如,假设有两个线程,一个线程对段 1 中的键值对进行操作,另一个线程对段 2 中的键值对进行操作,它们可以同时进行而不会相互干扰。

这种分段锁的设计能够有效减少锁的竞争。因为在多线程环境下,如果只有一个全局锁(像 Hashtable 那样),当多个线程同时访问哈希表时,就会频繁地争夺这个锁,导致性能下降。而在 ConcurrentHashMap 中,只有当多个线程同时访问同一个段时,才会出现锁竞争。

每个段内部的数据结构类似于 HashMap,它也有一个数组,用于存储键值对的位置信息。当一个线程要访问某个键值对时,首先会根据键的哈希值确定它所属的段,然后获取该段的锁,再进行相应的操作(如插入、删除或查找)。在操作完成后,会释放该段的锁。

例如,在一个多线程的缓存系统中,使用 ConcurrentHashMap 存储缓存数据。不同类型的缓存数据可以根据一定的规则分配到不同的段中。这样,当多个线程同时更新不同类型的缓存数据时,它们可以并行地进行操作,大大提高了缓存系统的并发性能。不过,在 Java 8 之后,ConcurrentHashMap 的实现进行了优化,除了保留部分分段锁的思想外,还结合了 CAS 操作和 synchronized 关键字来进一步提升性能和简化结构。

HashMap 如何解决 hash 冲突

HashMap 在存储键值对时,可能会出现不同的键计算出相同的哈希值的情况,这就是哈希冲突。它主要采用链地址法来解决这个问题。

当出现哈希冲突时,会在对应的数组位置(桶)上形成一个链表。例如,当两个不同的键 key1 和 key2 计算出相同的哈希值,并且这个哈希值对应的桶位置已经存储了一个键值对(假设是 (key1, value1)),那么当要存储 (key2, value2) 时,就会将 (key2, value2) 添加到这个桶位置的链表中。

在 Java 8 之后,当链表的长度达到一定阈值(默认为 8),链表会转换为红黑树。红黑树是一种自平衡的二叉搜索树,它可以提高在存在哈希冲突的桶中查找元素的效率。从链表转换为红黑树可以将查找时间复杂度从 O (n) 降低到 O (log n)。

当查找一个键时,首先会根据键的哈希值计算出它在数组中的位置。如果该位置只有一个键值对,直接比较键是否相等。如果该位置是一个链表,就需要遍历链表,逐个比较键是否相等,直到找到目标键或者遍历完整个链表。如果是红黑树,则按照红黑树的查找算法进行查找。

这种通过链表和红黑树结合的方式能够有效地处理哈希冲突,使得 HashMap 在大多数情况下都能保持较好的性能,即使在存在较多哈希冲突的情况下,也能够相对高效地进行存储和查找操作。

ArrayList 动态扩容过程,ArrayList LinkedList 区别

ArrayList 是一个动态数组,它的动态扩容过程是比较重要的一个特性。

当创建一个 ArrayList 时,它会有一个初始容量。当向 ArrayList 中添加元素时,如果元素的数量超过了当前的容量,就会触发扩容操作。它会创建一个新的数组,这个新数组的容量通常是原来数组容量的一定倍数(在 Java 中一般是 1.5 倍)。例如,初始容量为 10,当添加第 11 个元素时,就会创建一个新的容量为 15 的数组。

然后,将原来数组中的所有元素复制到新的数组中。这个复制过程是通过 System.arraycopy 方法或者类似的机制来实现的。这个方法可以高效地将一段连续的内存数据复制到另一个位置。在复制完成后,原来的数组就会被丢弃,新的数组成为 ArrayList 的存储容器,之后新添加的元素就可以存储到这个新数组中。

ArrayList 和 LinkedList 的区别主要体现在以下几个方面。

从存储结构上看,ArrayList 是基于数组的,它在内存中是一段连续的空间,而 LinkedList 是基于链表的,它的节点在内存中不需要连续存储,每个节点包含数据和指向下一个节点的引用。

在访问元素方面,ArrayList 可以通过索引快速访问元素,时间复杂度为 O (1)。例如,要访问 ArrayList 中的第 5 个元素,可以直接通过索引获取。而 LinkedList 访问元素需要从链表头开始逐个遍历,时间复杂度为 O (n)。

在插入和删除元素方面,ArrayList 在中间插入或删除元素时,需要移动后面的元素,这在元素数量较多时效率较低。而 LinkedList 在插入和删除元素时,只需要改变节点之间的引用关系,相对灵活,效率较高。

在空间占用方面,ArrayList 因为需要预留一定的初始容量,并且在扩容时可能会有一些未使用的空间,而 LinkedList 每个节点除了存储数据还需要存储引用,所以它们的空间占用情况也有所不同,具体取决于元素的数量和类型等因素。

链表和数组的区别,数组插入怎么做

链表和数组是两种不同的数据结构,它们有很多区别。

从存储方式上看,数组是一种连续存储的数据结构。它在内存中占用一段连续的空间,元素之间的存储位置是相邻的。例如,一个整数数组 int [] arr = {1, 2, 3, 4, 5},在内存中,这些整数是依次紧挨着存储的。而链表则是由节点组成的数据结构,每个节点包含数据和指向下一个节点的引用(在单向链表中)。例如,一个简单的链表节点结构可以包含一个数据域和一个 next 指针,节点可以分散在内存的不同位置。

在访问元素方面,数组具有高效的访问性能。因为它可以通过索引快速访问元素,访问一个元素的时间复杂度是 O (1)。例如,要访问 arr [3],可以直接定位到内存中的相应位置获取元素。而链表访问元素效率较低,由于它的存储结构是分散的,要访问链表中的某个节点,需要从链表头开始逐个遍历节点,时间复杂度是 O (n)。

在插入和删除元素方面,数组插入和删除元素相对复杂。当在数组中间插入一个元素时,需要将插入位置之后的元素依次向后移动。例如,要在上述数组的索引为 2 的位置插入一个数字 6。首先要创建一个新的足够大的数组(如果原数组已满),然后从最后一个元素开始,将索引大于等于 2 的元素往后移动一位,即先将 arr [4] 移动到 arr [5],arr [3] 移动到 arr [4],arr [2] 移动到 arr [3],最后将 6 赋值给 arr [2]。而链表插入和删除元素比较灵活,在链表中插入或删除一个节点,只需要改变相关节点的引用即可。例如,要在两个节点 A 和 B 之间插入一个节点 C,只需要将 A 的 next 指针指向 C,再将 C 的 next 指针指向 B。

在数组大小方面,数组的大小通常是固定的,在创建数组时就需要指定大小。虽然有些语言提供了动态数组的功能,但在底层实现上也是通过类似扩容的机制来改变大小。而链表的大小可以根据需要动态地增加或减少,只要有足够的内存来存储新的节点。

说说 https 过程,HTTPS 建立连接和传送数据的过程,https 数据是怎么加密的

HTTPS 是一种安全的超文本传输协议,它在 HTTP 的基础上加入了 SSL/TLS 加密协议。

首先是 HTTPS 建立连接的过程。当客户端(如浏览器)向服务器发起 HTTPS 请求时,它会先向服务器发送一个 Client Hello 消息。这个消息包含了客户端支持的 SSL/TLS 版本、加密算法套件列表、随机数等信息。例如,客户端可能支持 TLS 1.2、1.3 版本,并且列出一系列如 RSA、ECDHE 等加密算法。

服务器收到 Client Hello 消息后,会返回一个 Server Hello 消息。这个消息中会确定使用的 SSL/TLS 版本、加密算法套件以及另一个随机数。然后,服务器会发送自己的数字证书。这个数字证书包含了服务器的公钥,并且是由权威的证书颁发机构(CA)颁发的,用于证明服务器的身份。

客户端收到服务器的证书后,会验证证书的有效性。它会检查证书是否由信任的 CA 颁发,证书是否过期等。如果证书有效,客户端会使用证书中的公钥来加密一个随机生成的密钥(称为预主密钥),然后将加密后的预主密钥发送给服务器。

服务器使用自己的私钥来解密收到的预主密钥,这样客户端和服务器就共享了一个密钥。然后,双方会使用这个密钥以及之前交换的随机数,通过一系列复杂的密钥派生函数生成用于加密数据的会话密钥。

在传送数据的过程中,双方使用会话密钥进行对称加密。例如,当客户端向服务器发送数据时,会使用会话密钥对数据进行加密,然后发送加密后的密文。服务器收到密文后,使用相同的会话密钥进行解密,就可以得到原始的数据。

https 数据的加密主要是通过上述的对称加密和非对称加密相结合的方式。在建立连接阶段,使用非对称加密(公钥和私钥)来安全地交换对称加密的密钥,这样可以避免在网络上直接传输对称密钥而被窃取。然后,在实际的数据传输过程中,使用对称加密,因为对称加密的效率比非对称加密高很多,能够快速地对大量的数据进行加密和解密。

对称加密和非对称加密区别,常见算法

对称加密和非对称加密是两种不同的加密方式,它们在加密原理、密钥使用、安全性和性能等方面存在诸多区别。

对称加密使用相同的密钥进行加密和解密操作。也就是说,发送方用一个密钥对数据进行加密,接收方使用同样的密钥来解密获取原始数据。例如,在一个简单的文件加密传输场景中,发送方和接收方事先约定好一个密钥,如 “123456”,发送方用这个密钥将文件加密后发送,接收方收到加密文件后用同样的密钥解密。

其优点是加密和解密速度快,适用于对大量数据进行快速加密的场景。然而,它的缺点也很明显,密钥的管理和分发比较困难。因为在通信双方之间需要安全地传递这个唯一的密钥,如果密钥在传输过程中被窃取,那么数据的保密性就无法保证。常见的对称加密算法有 DES(Data Encryption Standard)、AES(Advanced Encryption Standard)等。DES 曾经是广泛使用的对称加密算法,但由于其密钥长度相对较短,安全性逐渐受到挑战,现在 AES 是更为常用的对称加密算法,它具有更高的安全性和更好的性能。

非对称加密则使用一对密钥,分别是公钥和私钥。公钥可以公开给任何人,用于对数据进行加密;而私钥只有持有者知道,用于解密用公钥加密的数据。例如,在一个网上银行的交易场景中,银行会公开其公钥,客户使用银行的公钥对交易信息进行加密后发送给银行,银行收到加密信息后,用自己的私钥进行解密。

非对称加密的优点是安全性高,因为公钥是公开的,即使被他人获取,也无法通过公钥解密数据,只有拥有私钥的一方才能解密。但其缺点是加密和解密速度相对较慢,不适合对大量数据进行加密。常见的非对称加密算法有 RSA、ECC(Elliptic Curve Cryptography)等。RSA 是应用较为广泛的非对称加密算法,它在数字签名、密钥交换等方面都有重要应用。

HTTP 与 HTTPS 的区别

HTTP(Hypertext Transfer Protocol)和 HTTPS(Hypertext Transfer Protocol Secure)都是用于在网络上传输数据的协议,但它们在安全性、连接方式、性能等方面存在明显区别。

从安全性角度来看,HTTP 是明文传输协议,数据在网络上是以未加密的形式传输的。这意味着在传输过程中,数据很容易被中间人窃取、篡改等。例如,当你在浏览器中输入用户名和密码登录一个网站时,如果该网站使用的是 HTTP 协议,那么你的用户名和密码可能会被网络中的不法分子获取。而 HTTPS 则是在 HTTP 的基础上加入了 SSL/TLS 加密协议,对数据进行了加密传输,确保了数据的保密性、完整性和身份验证。通过 SSL/TLS 协议,在传输数据之前会先建立一个安全的连接,使得数据在传输过程中以密文的形式存在,即使被截取也无法被轻易解读。

在连接方式上,HTTP 的连接建立相对简单,客户端向服务器发送请求,服务器收到请求后直接返回响应。而 HTTPS 在建立连接时,需要经过一系列复杂的握手过程。首先客户端向服务器发送 Client Hello 消息,包含支持的 SSL/TLS 版本、加密算法套件等信息;服务器收到后返回 Server Hello 消息,确定使用的 SSL/TLS 版本、加密算法套件并发送自己的数字证书;客户端验证证书后,使用证书中的公钥加密一个预主密钥发送给服务器;服务器用私钥解密得到预主密钥,然后双方通过一系列操作生成会话密钥,用于后续的数据加密传输。

从性能方面考虑,由于 HTTPS 需要进行加密和解密操作,以及额外的握手过程,所以其性能相对 HTTP 会有所下降。但是随着计算机硬件性能的提升和加密技术的优化,这种性能差距在逐渐缩小。而且在对安全性要求较高的场景下,如网上银行、电子商务等,HTTPS 的性能损耗是可以接受的,毕竟保障数据安全是首要任务。

另外,在 URL 的表现上也有区别,HTTP 的 URL 是以 “http://” 开头,而 HTTPS 的 URL 是以 “https://” 开头,用户可以通过 URL 的开头很容易地区分使用的是哪种协议。

TCP 三次握手过程,懂 TCP,那讲一下三次握手,TCP 三次握手

TCP(Transmission Control Protocol)三次握手是建立可靠的 TCP 连接的重要过程,它确保了通信双方都能确认对方的存在以及准备好进行数据传输。

第一次握手:客户端向服务器发送一个 SYN(Synchronize)包,这个包中包含客户端的初始序列号(Sequence Number),假设客户端的初始序列号为 x。SYN 包的主要作用是向服务器表明客户端希望建立一个 TCP 连接,并且告知服务器自己这边的初始序列号。此时客户端处于 SYN_SENT 状态,等待服务器的回应。

第二次握手:服务器收到客户端的 SYN 包后,会向客户端发送一个 SYN + ACK(Synchronize + Acknowledge)包。这个包中包含服务器自己的初始序列号,假设为 y,同时也包含对客户端初始序列号的确认,即 ACK = x + 1。这里的 ACK 值是在客户端的初始序列号基础上加 1,是为了告知客户端它收到了客户端的 SYN 包并且确认了客户端的初始序列号。此时服务器处于 SYN_RCVD 状态,等待客户端的进一步回应。

第三次握手:客户端收到服务器的 SYN + ACK 包后,会向服务器发送一个 ACK 包,其中 ACK = y + 1。这个 ACK 包是对服务器的初始序列号的确认,告知服务器它收到了服务器的 SYN + ACK 包并且确认了服务器的初始序列号。此时客户端和服务器都进入 ESTABLISHED 状态,意味着 TCP 连接已经成功建立,双方可以开始进行数据传输了。

通过这三次握手过程,双方可以确定对方都有能力进行数据传输并且都已经准备好了。例如,在一个网页浏览的场景中,当你在浏览器中输入网址并按下回车键时,浏览器(作为客户端)和网站服务器之间就会通过 TCP 三次握手建立连接,之后才会进行网页内容的传输。如果在握手过程中任何一方没有收到对方的回应或者回应不符合预期,那么就会认为连接建立失败,需要重新尝试建立连接。

TCP/UDP 的区别,UDP 有哪些应用场景,如何安全传输

TCP(Transmission Control Protocol)和 UDP(User Datagram Protocol)是两种不同的网络传输协议,它们在连接方式、可靠性、传输效率等方面存在诸多区别。

从连接方式上看,TCP 是面向连接的协议,在数据传输之前需要先建立一个可靠的连接,也就是通过前面提到的三次握手过程来确保双方都准备好进行数据传输。而 UDP 是无连接的协议,发送方不需要事先与接收方建立连接,就可以直接发送数据报。例如,在发送一封电子邮件时,通常使用 TCP 协议,需要先建立连接,确保邮件能准确无误地发送到对方邮箱;而在发送一些简单的实时数据,如网络游戏中的玩家位置信息,就可以使用 UDP 协议,因为不需要事先建立连接,能更快地将数据发送出去。

在可靠性方面,TCP 是可靠的传输协议。它通过一系列机制来保证数据的准确传输,比如序列号、确认号、重传机制等。当发送方发送数据后,会等待接收方的确认,如果在一定时间内没有收到确认,就会重新发送数据。而 UDP 是不可靠的传输协议,它不保证数据一定会被接收方准确接收,也没有重传机制。如果数据在传输过程中丢失或出现错误,发送方是不会知道的。

从传输效率上看,UDP 的传输效率相对较高。因为它不需要像 TCP 那样进行连接建立、维护和拆除等操作,也没有复杂的确认和重传机制,所以在发送数据时可以更快速地将数据发送出去。而 TCP 由于要保证数据的可靠性,在这些方面会花费更多的时间和资源,导致传输效率相对较低。

UDP 的应用场景主要有以下几类。一是实时性要求较高的场景,如在线游戏、视频直播等。在这些场景中,数据的实时更新非常重要,即使偶尔丢失一些数据,也不会对整体体验造成太大影响,所以可以使用 UDP 协议来快速发送数据。二是对资源消耗要求较低的场景,如一些简单的网络监测工具,只需要定期发送一些简单的数据报,不需要复杂的连接和确认机制,使用 UDP 可以节省资源。

要实现 UDP 的安全传输,可以采用一些加密和验证措施。比如,可以使用前面提到的对称加密或非对称加密算法对 UDP 传输的数据进行加密,确保数据的保密性。同时,可以使用数字签名等技术对数据进行身份验证,保证数据的来源可靠。另外,还可以通过在应用层添加一些协议来实现数据的完整性检查,防止数据在传输过程中被篡改。

网络分层模型,TCP、UDP 讲一讲

网络分层模型是为了更好地理解和设计网络通信系统而将网络通信功能划分成不同层次的一种架构。常见的网络分层模型有 OSI 七层模型和 TCP/IP 四层模型。

OSI 七层模型从下到上依次为物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。物理层主要负责在物理介质上传输原始的比特流,比如通过网线、光纤等传输电信号或光信号。数据链路层负责将物理层传来的比特流组装成帧,并进行差错控制和流量控制等操作。网络层主要负责确定数据从源节点到目的节点的路径,比如通过 IP 地址来进行路由选择。传输层则是我们重点关注的 TCP 和 UDP 所在的层次,它负责在不同主机之间提供端到端的通信服务,确保数据能准确无误地传输到目的地。会话层主要负责建立、维护和拆除会话,比如在一个长时间的网络会议中,会话层负责管理会议的开始、进行和结束等环节。表示层主要负责数据的格式转换、加密、解密等操作,比如将不同格式的文件转换成统一的格式进行传输。应用层是最上面的一层,它直接与用户应用程序打交道,比如浏览器、电子邮件客户端等应用程序都在应用层运行,通过调用下层的服务来实现各种网络功能。

TCP 和 UDP 都位于传输层。TCP 是一种面向连接、可靠的传输协议,前面已经详细介绍过它的三次握手建立连接以及通过序列号、确认号、重传机制等保证数据准确传输的特点。它适用于对数据可靠性要求较高的场景,如文件传输、网页浏览等。UDP 则是一种无连接、不可靠的传输协议,它不需要事先建立连接就可以直接发送数据报,传输效率相对较高,适用于实时性要求较高或对资源消耗要求较低的场景,如在线游戏、视频直播、网络监测工具等。在网络通信中,根据不同的应用需求,可以选择合适的传输协议来实现数据的传输。

Get 和 Post 有什么区别

Get 和 Post 是 HTTP 协议中两种常用的请求方法,它们在数据传输方式、安全性、数据大小限制、缓存等方面存在诸多区别。

从数据传输方式来看,Get 请求将参数数据附加在 URL 的末尾,通过 “?” 和 “&” 来分隔不同的参数。例如,在一个查询用户信息的 Get 请求中,URL 可能是 “https://example.com/user?name=John&age=30”,其中 “name=John” 和 “age=30” 就是传递的参数。这种方式使得参数信息直接暴露在 URL 中,任何人都可以看到。而 Post 请求则将数据放在请求体中发送,不会在 URL 中显示参数信息,相对更加隐蔽。

在安全性方面,由于 Get 请求的参数在 URL 中可见,所以在传输敏感信息(如密码、银行卡号等)时存在安全风险,这些信息可能会被记录在浏览器历史记录、服务器日志或者网络代理的缓存中。Post 请求相对更安全,因为数据在请求体中,不会轻易被查看。不过,这并不意味着 Post 请求就是绝对安全的,在网络传输过程中,如果没有进行加密(如使用 HTTPS),数据仍然可能被窃取。

对于数据大小限制,Get 请求对 URL 的长度有限制。虽然这个限制在不同的浏览器和服务器之间有所不同,但一般来说,URL 长度不能过长。这就限制了 Get 请求所能携带的数据量。而 Post 请求对数据大小的限制相对宽松,主要取决于服务器的配置和性能,理论上可以发送大量的数据。

在缓存方面,Get 请求通常会被浏览器和一些中间代理服务器缓存。这是因为 Get 请求主要用于获取数据,相同的 Get 请求可能会被多次发送以获取相同的资源,缓存可以提高访问效率。例如,当多次访问一个网站的静态图片时,浏览器可能会直接从缓存中获取,而不是再次向服务器发送请求。Post 请求一般不会被缓存,因为它通常用于提交数据,每次提交的数据可能都不同,缓存的意义不大。

常用的网络框架,用过的图片加载框架

常用的网络框架有很多,它们各有特点,适用于不同的场景。

Retrofit 是一个非常流行的网络框架,它基于 OkHttp 构建。Retrofit 的主要优势在于它可以将网络请求接口化,使得代码更加简洁和易于维护。通过定义接口,使用注解来描述网络请求的方法、参数、返回值等信息,然后由 Retrofit 自动生成实现类来处理网络请求。例如,在一个获取用户信息的接口中,可以使用 @GET 注解来表示这是一个 Get 请求,通过 {user_id} 这样的方式在 URL 中添加动态参数,返回值可以是一个自定义的用户信息对象。Retrofit 还支持多种数据格式的转换,如 JSON、XML 等,可以方便地与后端服务器进行数据交互。

OkHttp 也是广泛使用的网络框架,它是一个高效的 HTTP 客户端。它在底层处理网络连接、请求和响应等操作。OkHttp 的特点包括对 HTTP/2 的支持,这使得它在性能上有一定的优势,能够更快地建立连接和传输数据。它还提供了拦截器功能,可以在请求发送前和响应返回后进行一些额外的操作,如添加请求头、记录日志、处理缓存等。例如,在一个需要添加认证信息的网络请求中,可以通过拦截器来添加认证头,而不需要在每个请求中手动添加。

在图片加载框架方面,Glide 是一个功能强大的框架。它具有高效的缓存机制,可以缓存图片的原始数据、转换后的图片以及加载图片的结果。Glide 能够自动根据 ImageView 的大小来加载合适尺寸的图片,避免了加载过大的图片浪费资源。例如,在一个列表视图中,每个列表项都有一个 ImageView,Glide 会根据 ImageView 的大小自动调整图片的分辨率进行加载。它还支持多种图片格式,如 JPEG、PNG、GIF 等,并且可以加载本地资源和网络资源。

Picasso 也是一个常用的图片加载框架,它的使用方法比较简单。它的特点是可以很方便地实现图片的加载、缩放、裁剪等操作。与 Glide 类似,Picasso 也会自动处理图片的缓存,并且在图片加载失败时会提供默认的图片显示。例如,在一个应用的头像显示场景中,当用户头像加载失败时,Picasso 可以显示一个默认的头像图标。

简单说几个 Linux 命令,Linux dump 相关内容

Linux 中有许多常用的命令,这些命令可以帮助用户进行文件操作、系统管理、文本处理等各种任务。

ls 命令是最常用的命令之一,用于列出目录中的文件和子目录。例如,“ls -l” 可以以长格式列出文件的详细信息,包括文件类型、权限、所有者、大小、修改日期和文件名等内容。“ls -a” 则可以列出包括隐藏文件(文件名以 “.” 开头的文件)在内的所有文件。这在查看目录中的所有文件,特别是配置文件或者隐藏的系统文件时非常有用。

cd 命令用于切换目录。例如,“cd /home/user/Documents” 可以将当前工作目录切换到 “/home/user/Documents” 目录下。这个命令是在文件系统中导航的基本工具,通过它可以方便地进入不同的目录,访问所需的文件。

cp 命令用于复制文件或目录。例如,“cp file1.txt file2.txt” 可以将 “file1.txt” 文件复制为 “file2.txt”。如果要复制目录,可以使用 “cp -r dir1 dir2”,其中 “-r” 选项表示递归复制,会将 “dir1” 目录及其所有内容复制到 “dir2” 目录中。

Linux dump 相关内容主要涉及系统崩溃或者内存转储的情况。当 Linux 系统出现严重错误导致崩溃时,dump 文件可以记录系统当时的状态,包括内存中的数据、进程信息等内容。产生 dump 文件的方式通常是通过配置内核参数来实现。例如,通过设置 “/proc/sys/kernel/core_pattern” 参数可以指定 dump 文件的保存路径和格式。当系统崩溃时,会根据这个参数生成 dump 文件,然后可以使用一些专门的工具来分析这个文件,以找出系统崩溃的原因。

在分析 dump 文件时,常用的工具是 gdb(GNU Debugger)。它可以加载 dump 文件,查看内存中的数据结构、变量的值、栈信息等内容。例如,在一个程序因为段错误而崩溃的情况下,可以通过 gdb 分析 dump 文件来查看程序当时的调用栈,找到导致段错误的代码行。

正则表达式,写个正则把数字从字符串中提取出来

正则表达式是一种用于匹配、查找和替换文本中特定模式的工具。要从字符串中提取数字,可以使用以下正则表达式:“\d+”。

“\d” 在正则表达式中表示数字字符(0 - 9),“+” 表示前面的字符(在这里是数字字符)出现一次或多次。例如,对于字符串 “abc123def456”,使用这个正则表达式可以匹配到 “123” 和 “456”。

在很多编程语言中,可以使用相应的正则表达式库来实现数字的提取。以 Java 为例,使用 java.util.regex 包中的类可以实现。首先需要创建一个 Pattern 对象,通过 Pattern.compile 方法传入正则表达式 “\d+” 来编译这个模式。然后使用 Matcher 对象来进行匹配操作。例如:

import java.util.regex.Matcher;
import java.util.regex.Pattern;
class Main {
    public static void main(String[] args) {
        String text = "abc123def456";
        Pattern pattern = Pattern.compile("\\d+");
        Matcher matcher = pattern.matcher(text);
        while (matcher.find()) {
            System.out.println(matcher.group());
        }
    }
}

这段代码会输出 “123” 和 “456”,也就是从字符串中提取出来的数字部分。

MVP 讲一下

MVP(Model - View - Presenter)是一种软件设计模式,主要用于分离视图(View)、模型(Model)和表示层(Presenter)的职责,提高代码的可维护性和可测试性。

模型(Model)是负责数据的存储和处理的部分。它包含了业务逻辑和数据访问逻辑。例如,在一个用户管理系统中,模型层可能包含了用户数据的存储结构(如数据库表的设计),以及对用户数据进行增删改查的方法。模型层通常与数据库或者其他数据源进行交互,获取或更新数据,并且不关心数据如何在视图中显示。

视图(View)是用户界面部分,负责向用户展示数据和接收用户的操作。它可以是一个 Activity、Fragment 或者一个自定义的视图组件。视图层只负责显示数据和传递用户的操作事件,不包含业务逻辑。例如,在一个登录界面中,视图层就是用户看到的用户名输入框、密码输入框和登录按钮等组件,它会根据 Presenter 提供的数据来显示登录结果(如成功或失败的提示信息),并且当用户点击登录按钮时,将点击事件发送给 Presenter。

Presenter 是连接模型和视图的桥梁。它从视图接收用户的操作请求,调用模型层的方法来处理数据,然后将处理后的结果返回给视图进行显示。例如,在登录场景中,Presenter 会接收视图层发送的登录请求,调用模型层的方法来验证用户名和密码是否正确,然后根据验证结果通知视图层显示相应的提示信息。Presenter 不依赖于具体的视图实现,这使得视图层可以方便地进行替换或者修改,只要遵循相同的接口规范,就可以与 Presenter 正常协作。

这种模式的优点在于,它使得各个层的职责更加清晰。视图层专注于界面展示,模型层专注于数据处理,Presenter 层协调两者之间的交互。在测试方面,可以单独对模型层和 Presenter 层进行单元测试,因为它们不依赖于具体的视图实现,这样可以提高代码的质量和可维护性。同时,当业务逻辑发生变化时,只需要修改模型层和 Presenter 层的代码,视图层可以保持相对稳定;当界面需要更新时,只需要修改视图层的代码,而不会影响到模型层和 Presenter 层的核心业务逻辑。

看过哪些开源库源码,如 eventbus 和 glide

EventBus 源码分析:

EventBus 是一个用于 Android 和 Java 的事件发布 / 订阅库,它简化了组件间的通信。

在 EventBus 源码中,核心类是 EventBus 本身。它内部维护了一个订阅者列表,用于记录注册的订阅者以及他们感兴趣的事件类型。当通过 EventBus.register 方法注册一个订阅者时,EventBus 会通过反射机制分析订阅者类中的方法,找出那些带有特定注解(如 @Subscribe)且参数类型符合事件类型要求的方法,然后将这些方法和对应的订阅者信息添加到订阅者列表中。

例如,假设有一个 Activity 作为订阅者,它有一个方法标注为 @Subscribe 并接收一个自定义的 EventData 类型参数。当注册这个 Activity 时,EventBus 会识别出这个方法,并在后续有对应 EventData 类型事件发布时,能够准确地将事件传递给这个 Activity 的该方法。

发布事件时,通过 EventBus.post 方法。它会遍历订阅者列表,根据事件类型找到所有匹配的订阅者方法,然后通过反射调用这些方法来传递事件。这样就实现了事件从发布者到多个订阅者的传递,而不需要订阅者和发布者之间有直接的引用关系,解耦了组件间的通信。

EventBus 还考虑了线程切换的情况。可以通过注解参数或者配置来指定订阅者方法在哪个线程中执行,比如主线程、后台线程等。这对于在 Android 中确保 UI 更新在主线程进行等情况非常有用。

Glide 源码分析:

Glide 是一个强大的图片加载库。

其源码的核心在于 Glide 类,它是整个图片加载流程的入口。当调用 Glide.with 方法时,它会根据传入的上下文(如 Activity、Fragment 等)创建一个对应的 RequestManager 对象。这个 RequestManager 负责管理图片加载请求以及与生命周期相关的操作。

例如,在一个 Activity 中加载图片,Glide.with (Activity.this) 会返回一个适合该 Activity 生命周期的 RequestManager。如果 Activity 被暂停或销毁,RequestManager 能够相应地暂停或取消正在进行的图片加载请求,避免内存泄漏和不必要的资源浪费。

Glide 的图片加载过程涉及多个步骤。首先是从数据源(可以是网络、本地文件等)获取图片数据。它通过一系列的加载器(如 HttpUrlLoader 用于网络图片加载)来实现。这些加载器会根据数据源的类型执行相应的获取数据操作。

然后是对获取到的图片数据进行解码和转换。Glide 有自己的一套图片解码和转换机制,能够根据 ImageView 的大小和需求,将图片转换为合适的尺寸、格式等。例如,它可以将一张高清网络图片解码并缩放到适合手机屏幕显示的尺寸,同时还能处理图片的透明度、颜色等属性。

最后,将处理好的图片设置到 ImageView 中。在这个过程中,Glide 还会利用其高效的缓存机制。它有多种缓存级别,包括内存缓存(用于快速获取最近加载过的图片)和磁盘缓存(用于在设备重启等情况下仍能快速获取图片)。通过合理地运用这些缓存,Glide 能够大大提高图片加载的效率,减少重复加载相同图片的情况。

自定义 View 主要重写哪个方法

自定义 View 时,通常需要根据具体需求重写以下几个关键方法:

onMeasure 方法: 这个方法主要用于确定自定义 View 的大小。当父视图(ViewGroup)需要知道子视图的大小时,会调用子视图的 onMeasure 方法。在该方法中,需要根据自身的需求和父视图提供的约束条件来计算出合适的宽和高。例如,如果是一个自定义的圆形 View,可能需要根据父视图分配的空间以及自身设定的半径等因素来确定最终的尺寸。如果不重写 onMeasure 方法,可能会导致 View 的大小不符合预期,比如显示不完整或者占用过多不必要的空间。

onLayout 方法: 主要用于确定自定义 View 在父视图中的位置。当 onMeasure 阶段确定了 View 的大小后,onLayout 阶段会根据父视图的布局方式以及自身的大小来安排自己的位置。比如在一个自定义的布局容器中,有多个自定义 View,每个 View 都需要通过 onLayout 方法来明确自己相对于其他 View 和父视图的位置关系。不过,并非所有自定义 View 都需要重写 onLayout 方法,只有当 View 的位置需要根据特定规则进行动态调整或者在特定布局环境下确定时才需要重写。

onDraw 方法: 这是用于将自定义 View 的内容绘制到屏幕上的方法。在 onDraw 中,可以使用 Android 提供的绘图工具(如 Canvas、Paint 等)来绘制各种形状、文本、图像等。例如,要绘制一个自定义的图表 View,就需要在 onDraw 方法中根据数据和设计要求,使用 Canvas 绘制线条、柱状图等各种图形元素,并使用 Paint 来设置颜色、线条宽度等属性。如果没有重写 onDraw 方法,View 将不会显示出任何自定义的内容,只会显示默认的空白或者继承自父类的简单外观。

在实际的自定义 View 过程中,往往需要综合考虑这几个方法的重写,根据 View 的具体功能和外观设计来准确地确定其大小、位置和绘制内容。

触摸的传递机制

在 Android 中,触摸的传递机制涉及多个方法和不同层次的视图处理,主要通过以下方式实现:

当用户在屏幕上进行触摸操作时,触摸事件首先会被传递到最顶层的 Activity 的根视图(通常是一个 ViewGroup)。这个传递过程是从顶层开始,逐步向下传递到具体被触摸的视图。

dispatchTouchEvent 方法: 这是触摸事件分发的核心方法,存在于所有的视图(View 和 ViewGroup)中。它的主要作用是接收触摸事件并决定如何处理或继续传递该事件。对于 ViewGroup 来说,当触摸事件到达时,它会首先通过 dispatchTouchEvent 方法来判断是否要拦截该事件。如果在 ViewGroup 的 dispatchTouchEvent 方法中返回 true,表示拦截了触摸事件,那么后续的触摸事件处理流程将在该 ViewGroup 内部进行,不会再继续向下传递给子视图。如果返回 false,则会继续将触摸事件向下传递给子视图,通过调用子视图的 dispatchTouchEvent 方法来继续处理。

onInterceptTouchEvent 方法: 这个方法只存在于 ViewGroup 中,它是 ViewGroup 决定是否拦截触摸事件的关键方法。当触摸事件到达 ViewGroup 时,onInterceptTouchEvent 方法会被调用。如果在该方法中返回 true,就意味着该 ViewGroup 拦截了触摸事件,之后会通过该 ViewGroup 自己的 onTouchEvent 方法来处理触摸事件。如果返回 false,则会继续将触摸事件向下传递给子视图,让子视图有机会处理触摸事件。例如,在一个可滑动的列表视图(ListView)中,当用户在列表项上进行触摸操作时,如果列表视图的父视图(可能是一个包含 ListView 的布局容器)通过 onInterceptTouchEvent 方法拦截了触摸事件,那么列表项就无法再处理该触摸事件,而是由父视图来处理。

onTouchEvent 方法: 这是用于处理触摸事件的方法,存在于所有的视图中。当一个视图接收到触摸事件并且没有被父视图拦截时,就会通过 onTouchEvent 方法来处理该事件。例如,一个按钮(Button)在没有被父视图拦截触摸事件的情况下,当用户点击按钮时,按钮会通过 onTouchEvent 方法来处理点击事件,比如触发相应的点击操作(如启动一个 Activity、执行一段代码等)。

触摸事件的传递机制就是通过这几个关键方法在不同层次的视图之间进行协作,从而确保触摸事件能够准确地被相关视图处理,实现用户与 Android 设备的交互。

Java 实现生产者,消费者

在 Java 中,可以通过多种方式实现生产者 - 消费者模式,以下是一种常见的实现方式:

首先,需要定义一个共享资源类,这个类用于存储生产者生产的产品,并供消费者消费。例如:

class SharedResource {
    private int data;
    private boolean isProduced = false;
 
    public synchronized int getData() {
        while (!isProduced) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        isProduced = false;
        notifyAll();
        return data;
    }
 
    public synchronized void setData(int data) {
        while (isProduced) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.data = data;
        isProduced = true;
        notifyAll();
    }
}

在这个共享资源类中,有一个整型数据成员 data 用于存储生产的产品,还有一个布尔型变量 isProduced 用于表示是否已经生产了产品。通过 synchronized 关键字保证了对共享资源的访问是同步的,避免了多个线程同时访问造成的数据不一致问题。

然后,定义生产者类:

class Producer implements Runnable {
    private SharedResource sharedResource;
 
    public Producer(SharedResource sharedResource) {
        this.sharedResource = sharedResource;
    }
 
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            int producedData = i;
            sharedResource.setData(producedData);
            System.out.println("生产者生产了: " + producedData);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

生产者类实现了 Runnable 接口,在其 run 方法中,通过调用共享资源类的 setData 方法来生产产品,并将生产的数据传递给共享资源。每次生产后会暂停一段时间(这里是 1 秒),模拟生产过程中的时间间隔。

接着,定义消费者类:

class Consumer implements Runnable {
    private SharedResource sharedResource;
 
    public Consumer(SharedResource sharedResource) {
        this.sharedResource = sharedResource;
    }
 
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            int consumedData = sharedResource.getData();
            System.out.println("消费者消费了: " + consumedData);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

消费者类同样实现了 Runnable 接口,在其 run 方法中,通过调用共享资源类的 getData 方法来消费产品,并输出消费的数据。每次消费后也会暂停一段时间(这里是 2 秒),模拟消费过程中的时间间隔。

最后,在主程序中启动生产者和消费者线程:

public class Main {
    public static void main(String[] args) {
        SharedResource sharedResource = new SharedResource();
        Producer producer = new Producer(sharedResource);
        Consumer consumer = new Consumer(sharedResource);
 
        Thread producerThread = new Thread(producer);
        Thread consumerThread = new Thread(consumer);
 
        producerThread.start();
        consumerThread.start();
    }
}

通过这种方式,生产者线程不断地生产产品并将其放入共享资源中,消费者线程则不断地从共享资源中获取产品并消费,实现了生产者 - 消费者模式的基本功能,同时通过 synchronized 关键字和 wait、notifyAll 等方法保证了线程之间的协调和数据的一致性。

Lock 和 syncrognized 原理区别,适合什么场景

原理区别:

synchronized 关键字: synchronized 是 Java 内置的一种锁机制,它基于对象头中的锁标志位和监视器(Monitor)来实现同步。当一个线程进入一个 synchronized 方法或者代码块时,它会获取对象的监视器锁。这个监视器锁实际上是一个对象关联的特殊结构,它记录了锁的状态等信息。

例如,当一个类中有一个 synchronized 方法,多个线程试图访问这个方法时,第一个进入的线程会获取该方法所属对象的监视器锁,其他线程则需要等待这个线程释放锁后才能进入。在对象头中,锁标志位会根据锁的状态进行相应的变化,比如从无锁状态变为偏向锁、轻量级锁或重量级锁等,以适应不同的并发场景。

Lock 接口及其实现类(如 ReentrantLock): Lock 接口提供了比 synchronized 更灵活的锁机制。它是通过 AQS(AbstractQueuedSynchronizer)来实现的。AQS 是一个抽象的同步框架,它维护了一个等待队列,用于存储等待获取锁的线程。

例如,ReentrantLock 是 Lock 接口的一个常用实现类。当一个线程试图获取 ReentrantLock 锁时,如果锁已经被其他线程获取,那么这个线程会被放入 AQS 维护的等待队列中,按照一定的顺序等待锁的释放。ReentrantLock 还提供了一些额外的功能,比如可以实现公平锁(按照线程请求锁的顺序来分配锁)和非公平锁(不保证线程请求锁的顺序),通过其构造函数可以指定是公平锁还是非公平锁。

适合场景:

synchronized 适合的场景:

  • 简单的同步需求:当只需要对一个方法或者代码块进行简单的同步,防止多个线程同时访问时,synchronized 关键字使用起来非常方便。例如,在一个简单的计数器类中,有一个方法用于增加计数器的值,为了防止多个线程同时修改计数器的值导致数据不一致,就可以将这个方法声明为 synchronized 方法。
  • 对性能要求不是特别高的场景:虽然 synchronized 在高并发场景下可能会因为锁升级等原因导致一定的性能损失,但在一些对性能要求不是特别高的并发场景下,它能够满足基本的同步需求。比如在一个小型的单机应用中,偶尔会有几个线程同时访问一些共享资源,使用 synchronized 就足够了。

Lock 适合的场景:

  • 需要更灵活的锁机制:当需要实现公平锁或非公平锁等特殊的锁机制时,Lock 接口及其实现类就非常有用。例如,在一个排队系统中,希望按照客户到达的顺序来分配服务资源,就可以使用 ReentrantLock 实现公平锁,保证每个客户按照先后顺序获得服务。
  • 高性能并发场景:在一些高并发、对性能要求较高的场景下,Lock 接口提供的可重入性、可选择性的锁获取方式以及能够避免不必要的锁升级等特点,可以提高并发处理的效率。比如在一个大型的网络服务器应用中,处理大量的并发请求,使用 ReentrantLock 等 Lock 接口的实现类可以更好地优化锁的使用,提高系统的整体性能。
  • 可中断的锁获取:Lock 接口的一些实现类(如 ReentrantLock)允许线程在获取锁的过程中,如果等待时间过长,可以中断等待并采取其他措施。这在一些需要及时响应外部条件变化的场景下非常有用。例如,在一个实时监控系统中,如果一个线程等待获取锁的时间超过了规定的时间,就可以中断等待,去执行其他紧急的任务。
最近更新:: 2025/10/22 15:36
Contributors: luokaiwen