rokevin
移动
前端
语言
  • 基础

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

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

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

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

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

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

  • Android 四大组件是什么?
  • BroadcastReceiver 的使用场景有哪些?
  • BroadcastReceiver 能给其他应用发送消息吗?
  • ContentProvider 是干什么用的?
  • 说下 Service 有哪些实现方式及其区别、Service 生命周期、IntentService 原理。
  • Service 的实现方式及区别
  • Service 生命周期
  • IntentService 原理
  • Activity 启动模式有哪些?onNewIntent 是怎么回事?
  • Activity 启动模式
  • onNewIntent 是怎么回事?
  • 安卓中 TaskAffinity 是干啥用的?
  • 比较一下 ListView 和 RecyclerView,RecyclerView 底层如何实现 Item 复用?两个 RecyclerView 嵌套会有什么问题?
  • ListView 和 RecyclerView 的比较
  • RecyclerView 底层的 Item 复用机制
  • 两个 RecyclerView 嵌套会出现的问题
  • 讲讲 Activity 和 Service 的通信方式。
  • 通过绑定服务(Bound Service)通信
  • 通过广播(Broadcast)通信
  • 通过共享文件或数据库通信
  • Android 生命周期相关,如 onPause 和 onStop 具体是怎么调用的?调用一个透明的 activity,其生命周期怎么变化?屏幕转置时生命周期怎么调用?
  • onPause 和 onStop 的调用情况
  • 透明 Activity 的生命周期变化
  • 屏幕转置时生命周期的调用
  • 说一下 Broadcast 的种类、注册的方式,以及不同注册方式的生命周期。
  • Broadcast 的种类
  • Broadcast 的注册方式
  • 不同注册方式的生命周期
  • 局部广播和全局广播的区别分别是用什么实现的?
  • 由 Android 的 Binder 聊到 IPC 通信,Android 中进程间通信的方式有哪些?为什么是基于 Binder 的,不用传统的操作系统进程间通信方式呢?一个 app 可以开启多个进程吗?怎么做呢?每个进程都是在独立的虚拟机上吗?
  • Android 中进程间通信(IPC)的方式
  • 为什么 Android 主要基于 Binder 进行 IPC 而不是传统方式
  • 一个 app 可以开启多个进程吗?怎么做呢?
  • 每个进程都是在独立的虚拟机上吗?
  • 内核态与用户态是什么?他们有什么区别?
  • 产生死锁需要哪些条件?
  • Android 热修复相关知识。
  • 用 MultiDex 解决何事?其根本原因在于?Dex 如何优化?主 Dex 放哪些东西?主 Dex 和其他 Dex 调用、关联?Odex 优化点在于什么?
  • MultiDex 的用途和根本原因
  • Dex 优化
  • 主 Dex 内容
  • 主 Dex 和其他 Dex 的调用与关联
  • Odex 优化点
  • Dalvik 和 Art 虚拟机区别?
  • 多渠道打包如何实现 (Flavor、Dimension 应用)?从母包生出渠道包实现方法?渠道标识替换原理?
  • 多渠道打包的实现(Flavor 和 Dimension)
  • 从母包生出渠道包实现方法
  • 渠道标识替换原理
  • Android 打包哪些类型文件不能混淆?
  • Retrofit 主要实现机制?Retrofit 的作用、原理。
  • Retrofit 的作用
  • Retrofit 的原理
  • 动态代理静态代理区别?
  • 模块化怎么做?怎么设计?接口发现## 暴漏## 怎么做?基于什么基本思想?
  • 模块化的实现方法
  • 模块化的设计
  • 接口发现和暴露
  • 基本思想
  • MVC、MVP、MVVM 应用和彼此本质区别?
  • MVC(Model - View - Controller)的应用和特点
  • MVP(Model - View - Presenter)的应用和特点
  • MVVM(Model - View - ViewModel)的应用和特点
  • 本质区别
  • Glide 缓存特点,Glide 原理,大图如何压缩?
  • Glide 缓存特点
  • Glide 原理
  • 大图如何压缩?
  • 开源库源码、Framework 源码了解多少?
  • 安卓中哪些地方用到了观察者模式?
  • 介绍一下所有的 map,以及## 他们## 之间的对比、适用场景。
  • HashMap
  • LinkedHashMap
  • TreeMap
  • Hashtable
  • ConcurrentHashMap
  • Android 11 有什么新的特性?
  • 隐私保护增强
  • 通信优化
  • 屏幕和显示改进
  • 安卓的新特性。
  • 跨设备互联增强
  • 系统性能优化
  • 人工智能和机器学习集成
  • 讲一下 Handler,MessageQueue 的排序方式是什么,用 post 和 postDelay,message 加入到 messagequeue 有什么不同?Handler 底层,postDelay 源码。
  • MessageQueue 的排序方式
  • post 和 postDelay 的区别
  • Handler 底层机制
  • postDelay 源码分析
  • Handler 的工作流程,Handler 能否在子线程初始化以及用什么方案来替代 Handler 在子线程初始化?
  • Handler 工作流程
  • Handler 在子线程初始化问题及替代方案
  • Handler 如何切换线程?Handler 如何回调?Handler 注意事项 (预防内存泄漏),Handler 封装使用 (handlerThread、IntentService)。
  • Handler 切换线程的方式
  • Handler 回调机制
  • Handler 注意事项(预防内存泄漏)
  • Handler 封装使用(HandlerThread、IntentService)
  • HandlerThread:如前面提到的,HandlerThread 是一个自带 Looper 的线程。它的使用场景主要是在需要在子线程中处理消息的情况。例如,在一个文件下载任务中,可以创建一个 HandlerThread,然后在这个线程中通过 Handler 来处理下载进度的更新消息。当文件下载进度发生变化时,通过 Handler 发送消息,在 HandlerThread 中的 Handler 处理这些消息,更新下载进度的显示,这样就不会阻塞主线程。
  • IntentService:IntentService 是一种特殊的 Service,它内部使用了 Handler 和 HandlerThread 来处理异步任务。当通过 startService 方法启动 IntentService 时,它会将传入的 Intent 放入一个工作队列,然后通过 Handler 和 HandlerThread 来依次处理这些 Intent。例如,在一个批量图片处理的应用中,可以通过 startService 方法传入包含图片路径等信息的 Intent,IntentService 会在后台线程(通过 HandlerThread)中处理这些图片,处理完成后自动停止服务。这种方式简化了在 Service 中处理异步任务的过程,并且由于是在后台线程处理,不会阻塞主线程。
  • 为什么主线程 loop 不会 ANR?ThreadLocal 原理。
  • 主线程 loop 不会 ANR 的原因
  • ThreadLocal 原理
  • launcher 应用抽屉,之前有个毛玻璃效果背景,从系统 ROM 角度说下怎么做?实时的睡眠水面倒影效果怎么做?实时更新的 UI 性能如何保证?
  • Launcher 应用抽屉毛玻璃效果背景的实现(从系统 ROM 角度)
  • 实时的睡眠水面倒影效果的实现
  • 实时更新的 UI 性能保证
  • UI 基础:Measure、Layout、draw 大流程、绘制顺序,FlowLayout 怎么实现?
  • UI 的 Measure、Layout、draw 大流程
  • Measure(测量)流程
  • Layout(布局)流程
  • Draw(绘制)流程
  • 绘制顺序
  • FlowLayout 的实现
  • TCP 和 UDP 的区别以及所属协议层
  • OSI 七层网络模型介绍及各层协议举例和五层模型
  • TCP 是怎么保证的可靠传输
  • 三次握手的过程、四次挥手的过程以及原因
  • 三次握手过程
  • 四次挥手过程
  • 为什么是三次握手和四次挥手
  • HTTP 和 HTTPS 及其区别、HTTP 请求格式和 HTTPS 的流程、原理、握手过程
  • HTTP(超文本传输协议)和 HTTPS(超文本传输安全协议)区别
  • HTTP 请求格式
  • HTTPS 的流程、原理和握手过程
  • HTTP 里面有哪些常用的方法?GET 与 POST 的区别以及 Get 和 Post 有什么区别?
  • DNS 的解析过程以及服务器 IP 地址改变后客户端如何知晓?
  • 一个 URL 输入到浏览器到显示网页的过程?
  • TLS 握手相关知识
  • 聊聊数据结构吧,二叉树有什么好处?有什么具体的应用场景?
  • HashMap 底层是怎么实现的?有哪些遍历方法?是否线程安全?哪些是线程安全的?讲一下 rehash。
  • ConcurrentHashMap 的原理?
  • ArrayList 的扩容能说一说吗?
  • 比较一下 Java 中的 set、list 和 map。
  • 多线程了解吗?在单核 CPU 的情况下,有多个任务,那是单线程执行的时间快还是多线程快呢?
  • Android 里面多线程的应用,线程池相关知识。
  • 进程和线程的区别,并发和并行分别是什么意思?多线程是并发还是并行?
  • 如何保证多线程安全?synchronized 和 ReentrantLock 的区别,你认为哪一种比较好,为什么?synchronized 和 Lock 区别(性能,synchronized 具体做了哪些优化),synchronized 的底层原理?类锁与对象锁的区别?
  • 线程之间可以共享什么?
  • volatile 理解,JMM 中主存和工作内存到底是啥?和 JVM 各个部分怎么个对应关系?
  • CAS、各种锁相关知识。
  • 线程 a 等 b,b 等 c 的实现。
  • 设计模式,结合业务谈谈你熟悉的设计模式,有用过哪些设计模式?
  • 单例模式,问了下双重校验锁的实现和静态内部类的 volatile。
  • 简单工厂、抽象工厂是什么?
  • 责任链模式相关,如 retrofit intercept 操作了解么?让你设计拦截器怎么设计?
  • 了解过 Java 虚拟机吗 JVM?Java 文件具体是怎么运行的?
  • JVM 垃圾回收,内存管理,新生代老年代都说一下。
  • Java 中 public,protected,default(什么也不写),private 的区别,子类可以继承父类哪些访问限定符修饰的方法(public,protected,default(什么也不写)),如何使得一个函数不被覆写(final)。
  • 静态内部类和匿名内部类的区别,内部类如何调用外部类的方法(Outter.this. 方法名)。
  • 内存泄漏与内存溢出关系,判断对象是否已死(两次标记:可达性分析 + finalize 方法),详细说 java 四种引用(强引用,软引用,弱引用,虚引用)。
  • Java 泛型,反射。
  • 类的 equals 重写。
  • OOM 遇到过么,怎么确认位置。
  • 技术上的最大突破。
  • 内部类会有内存泄漏问题吗?内部类为什么能访问外部类的变量,为什么还能访问外部类的私有变量。
  • 手机要下载视频,你该怎么设计,需要考虑哪些因素?下载后的回调函数该放在子线程还是主线程中?
  • 接口和抽象的理解。
  • 如何理解依赖反转?如何理解多态?
  • 进程间通信的方式,操作系统进程间通信方式?这些方式间的区别?
  • 同步与异步的区别?
  • 线程的中断,try/catch 怎么处理异常啥的,try catch finally 返回值问题。
  • 非对称加密相关(结合 HTTPS 讲)

字节跳动 面试

Android 四大组件是什么?

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

Activity 是 Android 应用中最基本的组件,用于实现用户界面。它可以包含各种视图控件,如按钮、文本框等。一个 Activity 通常对应一个屏幕的内容。用户可以通过点击、滑动等操作在不同的 Activity 之间进行切换。例如,在一个社交应用中,登录界面是一个 Activity,主界面是另一个 Activity,当用户成功登录后就会从登录 Activity 跳转到主界面 Activity。

Service 主要用于在后台执行长时间运行的操作,不提供用户界面。它可以用于执行一些不依赖于用户交互的任务,比如音乐播放服务。即使应用的界面被关闭,音乐播放服务依然可以在后台持续运行,让用户能够继续收听音乐。而且 Service 还可以与其他组件进行通信,如通过绑定服务的方式,让 Activity 可以控制 Service 的状态。

Broadcast Receiver 用于接收系统或者应用发出的广播消息。例如,当电池电量变化、网络连接变化或者系统开机等事件发生时,系统会发送相应的广播,Broadcast Receiver 就可以接收这些广播并做出相应的反应。像手机开机后,一些应用会通过 Broadcast Receiver 接收开机广播,然后自动启动一些后台服务或者进行数据更新等操作。

Content Provider 用于在不同的应用之间共享数据。它可以把自己的数据提供给其他应用访问,同时也可以访问其他应用通过 Content Provider 暴露的数据。比如,联系人应用可以通过 Content Provider 将联系人数据提供给短信应用,这样短信应用在发送短信时就可以获取联系人信息来填充收件人字段。

BroadcastReceiver 的使用场景有哪些?

Broadcast Receiver 有许多使用场景。

首先是系统事件监听方面。当系统的网络状态发生改变时,如从 Wi - Fi 切换到移动数据或者反之,应用可以通过 Broadcast Receiver 接收网络变化广播,然后在代码中做出相应的调整。比如,一个视频播放应用,如果检测到网络从 Wi - Fi 切换到移动数据,可能会提示用户是否继续播放高清视频,因为移动数据可能会产生流量费用。另外,当系统的电池电量发生变化时,也可以通过 Broadcast Receiver 接收广播。例如,当电池电量低于一定阈值时,应用可以自动暂停一些非关键的后台任务,如数据同步任务,以节省电量。还有系统开机广播,应用可以在接收到开机广播后自动启动一些服务,如消息推送服务,及时向用户推送最新的消息。

其次是应用内通信场景。在一个大型的应用中,不同的模块可能需要相互通信。例如,一个文件下载模块完成下载任务后,可以发送一个自定义广播,而其他模块中的 Broadcast Receiver 接收到这个广播后,就可以对下载完成的文件进行后续处理,如文件解压或者将文件添加到特定的列表中展示给用户。

再者是跨应用通信场景。虽然这种情况相对较少,但也有应用。例如,一个安全应用检测到设备安装了新的应用,它可以发送广播,而其他有相应 Broadcast Receiver 的应用可以接收到这个广播,来判断是否需要对新安装的应用进行一些安全扫描或者兼容性检查等操作。

BroadcastReceiver 能给其他应用发送消息吗?

Broadcast Receiver 本身主要是用于接收广播消息,但在 Android 系统中,它可以间接实现向其他应用发送消息的功能。

一种方式是通过发送自定义广播来实现。应用可以定义自己的广播动作(action),然后在需要发送消息的时候,通过 Intent 来发送这个自定义广播。例如,应用 A 定义了一个名为 “com.example.myapp.MESSAGE_SENT” 的广播动作,当应用 A 中有一个事件发生,比如完成了一次数据更新,它可以通过以下代码发送广播:

Intent intent = new Intent("com.example.myapp.MESSAGE_SENT");
sendBroadcast(intent);

如果其他应用(假设是应用 B)注册了接收这个广播动作的 Broadcast Receiver,就可以接收到应用 A 发送的消息。在应用 B 中,注册 Broadcast Receiver 的代码可能如下:

public class MyBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if ("com.example.myapp.MESSAGE_SENT".equals(intent.getAction())) {
            // 接收到消息后的处理逻辑
            Toast.makeText(context, "收到来自其他应用的消息", Toast.LENGTH_SHORT).show();
        }
    }
}

在 AndroidManifest.xml 文件中,还需要对 Broadcast Receiver 进行注册:

<receiver android:name=".MyBroadcastReceiver">
    <intent - filter>
        <action android:name="com.example.myapp.MESSAGE_SENT" />
    </intent - filter>
</receiver>

不过需要注意的是,这种方式发送广播是一种比较松散的通信方式。如果发送的广播是系统广播或者比较通用的自定义广播,可能会有多个应用接收到并进行处理,这可能会导致一些不可预期的结果。而且从安全角度考虑,Android 8.0(API 级别 26)及以上版本对隐式广播进行了限制,一些系统广播不能随意发送,以减少系统资源的浪费和安全风险。

ContentProvider 是干什么用的?

Content Provider 是 Android 用于在不同应用之间共享数据的组件。

从数据共享的角度来看,它就像是一个数据桥梁。假设应用 A 有一个数据库,存储了用户的联系人信息,而应用 B 是一个短信应用,需要获取联系人信息来填充收件人字段。此时,应用 A 可以通过 Content Provider 将联系人数据暴露给其他应用。Content Provider 通过定义一套标准的接口,允许其他应用使用 ContentResolver 来访问其管理的数据。例如,应用 B 可以使用以下代码通过 ContentResolver 来查询应用 A 提供的联系人数据:

ContentResolver contentResolver = getContentResolver();
Uri uri = Uri.parse("content://com.example.appA.contacts/");
Cursor cursor = contentResolver.query(uri, null, null, null, null);
if (cursor!= null) {
    while (cursor.moveToNext()) {
        String name = cursor.getString(cursor.getColumnIndex("name"));
        String phoneNumber = cursor.getString(cursor.getColumnIndex("phone_number"));
        // 对获取到的联系人信息进行处理
    }
    cursor.close();
}

在这个例子中,“content://com.example.appA.contacts/” 是一个自定义的 Content Provider 的 URI,用于标识数据的来源。Content Provider 内部会根据这个 URI 以及传入的查询参数(如上面代码中的 null 参数部分,也可以指定具体的查询条件)来从自己管理的数据源(如数据库)中获取数据,并返回给调用者。

Content Provider 不仅可以用于共享数据库数据,还可以共享文件等其他类型的数据。例如,一个文件管理应用可以通过 Content Provider 将存储在设备中的文件信息共享给其他应用,其他应用可以通过 Content Provider 获取文件的元数据,如文件大小、类型等,甚至在权限允许的情况下,还可以获取文件的内容。

此外,Content Provider 还可以实现数据的增删改操作。除了查询操作(query 方法),还可以通过 ContentResolver 的 insert 方法来插入数据、update 方法来更新数据和 delete 方法来删除数据。这使得其他应用可以对共享的数据进行全面的操作,当然,这些操作的权限是由 Content Provider 的实现者来控制的,Content Provider 可以根据不同的权限设置,允许或者拒绝其他应用的操作请求。

说下 Service 有哪些实现方式及其区别、Service 生命周期、IntentService 原理。

Service 的实现方式及区别

** Started Service(启动式服务):这种方式是当其他组件(如 Activity)通过 startService () 方法启动 Service 时使用。例如,一个音乐播放应用,当用户点击播放按钮时,Activity 可以通过 startService () 启动音乐播放服务。这种服务一旦启动,就会在后台独立运行,即使启动它的组件(如 Activity)被销毁,服务依然会继续运行。它主要用于执行一些不需要与启动组件进行交互的长时间运行的任务,比如后台数据下载、音乐播放等。不过,启动式服务不能直接将运行结果返回给启动它的组件。 ** Bound Service(绑定式服务):通过 bindService () 方法来实现。它允许其他组件(如 Activity)与 Service 进行绑定,从而可以和 Service 进行通信和交互。例如,一个传感器数据获取服务,Activity 绑定这个服务后,可以实时获取传感器数据并在界面上进行显示。绑定式服务的生命周期和绑定它的组件紧密相关,当所有绑定的组件都解除绑定后,服务就会停止。它的特点是可以在组件和服务之间进行方法调用,方便数据的交互和共享。

Service 生命周期

** onCreate():当服务第一次被创建时调用。这是进行一次性初始化操作的好时机,比如初始化一些变量、加载资源等。例如,在一个数据同步服务中,可以在 onCreate () 中初始化数据库连接。 ** onStartCommand():当通过 startService () 方法启动服务时会调用这个方法。这个方法可以接收 Intent 参数,通过 Intent 可以传递一些启动服务所需的参数。例如,在下载服务中,可以通过 Intent 传递下载文件的 URL 等参数。服务根据这个方法的返回值来确定在系统资源紧张时如何处理服务,返回值有 START_STICKY、START_NOT_STICKY 等不同选项。如果返回 START_STICKY,当服务因为内存不足等原因被系统销毁后,系统会尝试重新创建服务。 ** onBind():当通过 bindService () 方法绑定服务时调用。这个方法返回一个 IBinder 对象,用于实现服务和绑定组件之间的通信接口。例如,在一个位置服务中,通过 onBind () 返回的 IBinder 对象可以让绑定的 Activity 获取当前位置信息。 ** onUnbind():当所有绑定的组件都解除与服务的绑定后调用。这可以用于进行一些清理工作,比如释放一些资源。 ** onDestroy()**:当服务被销毁时调用。可以在这里释放所有占用的资源,如关闭数据库连接、停止线程等。

IntentService 原理

IntentService 是 Service 的一个子类,它主要用于处理异步请求。它内部有一个工作线程来处理通过 startService () 传递过来的 Intent 请求。当通过 startService () 启动 IntentService 时,它会将 Intent 添加到一个工作队列中,然后依次处理队列中的 Intent。

它的优点是简化了异步任务的处理。例如,一个批量图片处理应用,每次用户选择一组图片进行处理(如添加滤镜)时,可以通过 startService () 启动 IntentService,并将图片的相关信息(如路径等)通过 Intent 传递给 IntentService。IntentService 会在后台线程中依次处理这些图片,而不会阻塞主线程。在处理完所有请求后,IntentService 会自动停止,不需要手动管理服务的停止,这减少了代码的复杂性和出错的可能性。同时,由于它是在工作线程中处理任务,避免了在主线程中执行耗时操作导致应用无响应(ANR)的问题。

Activity 启动模式有哪些?onNewIntent 是怎么回事?

Activity 启动模式

** standard(标准模式):这是默认的启动模式。在这种模式下,每次启动一个 Activity,都会创建一个新的实例。例如,假设应用中有一个 Activity A,当从一个地方多次启动 Activity A 时,会创建多个 Activity A 的实例并添加到任务栈中。就像在一个购物应用中,商品详情页面可能是一个 Activity,每次用户从不同的商品列表进入商品详情页面时,都会创建一个新的商品详情 Activity 实例。 ** singleTop(栈顶复用模式):如果要启动的 Activity 已经位于任务栈的栈顶,那么就不会重新创建这个 Activity 的实例,而是会调用这个栈顶 Activity 的 onNewIntent () 方法。例如,在一个浏览器应用中,浏览器的主页面 Activity 如果采用 singleTop 模式,当用户在浏览器主页面通过搜索框再次打开这个主页面(可能是因为搜索结果的跳转),就不会重新创建主页面 Activity,而是会复用栈顶的这个 Activity 实例,并且通过 onNewIntent () 方法传递新的 Intent,这样可以更新页面内容等操作。 ** singleTask(栈内复用模式):当启动一个 Activity 时,如果任务栈中已经存在这个 Activity 的实例,那么会将这个 Activity 之上的其他 Activity 全部清除,然后复用这个已存在的 Activity 实例。例如,在一个应用中有登录 Activity 和主界面 Activity,登录 Activity 采用 singleTask 模式。当用户从主界面退出登录后,再次登录时,登录 Activity 不会重新创建,而是会复用之前的登录 Activity 实例,并且清除它上面的其他 Activity(如主界面 Activity),这样可以保证登录流程的独立性和数据的一致性。 ** singleInstance(单实例模式):这种模式下,会为这个 Activity 创建一个单独的任务栈。这个 Activity 在整个系统中只有一个实例。例如,在一个应用中调用系统的拨号 Activity(假设这个拨号 Activity 是通过自定义的方式设置为 singleInstance 模式),这个拨号 Activity 会在自己单独的任务栈中,当其他应用也需要调用拨号功能时,会复用这个已经存在的拨号 Activity 实例,而不是重新创建。这样可以保证拨号功能的独立性,并且方便不同应用之间共享这个 Activity。

onNewIntent 是怎么回事?

onNewIntent () 方法是在 Activity 已经存在实例并且通过新的 Intent 启动时调用。当 Activity 的启动模式为 singleTop 或者 singleTask 时,就有可能会调用这个方法。

当调用 onNewIntent () 方法时,意味着 Activity 有了新的启动意图。例如,一个消息接收 Activity,它的启动模式是 singleTop。当有新消息到达时,系统通过新的 Intent 启动这个 Activity,此时就会调用 onNewIntent () 方法。在这个方法中,可以获取新的 Intent,从中提取消息内容等相关信息,然后更新 Activity 的界面显示。

它的主要作用是在不重新创建 Activity 的情况下,更新 Activity 的状态或者数据。如果没有 onNewIntent () 方法,当 Activity 需要根据新的 Intent 进行更新时,可能需要重新创建 Activity,这会导致一些不必要的资源浪费和状态丢失。例如,一个正在播放视频的 Activity,当通过新的 Intent 传递新的视频播放链接时,通过 onNewIntent () 方法就可以在不重新创建 Activity 的情况下切换视频播放内容,保持播放进度等相关状态。

安卓中 TaskAffinity 是干啥用的?

TaskAffinity 主要用于定义 Activity 所属的任务栈。在 Android 系统中,任务栈是用于管理 Activity 的栈结构。

当一个 Activity 被启动时,它会被放置到一个任务栈中。TaskAffinity 属性可以指定 Activity 希望归属的任务栈。默认情况下,一个应用的所有 Activity 都具有相同的 TaskAffinity,也就是应用的包名,这使得它们通常都在同一个任务栈中。然而,通过修改 TaskAffinity 属性,可以改变这种默认行为。

例如,在多任务处理场景下,假设有一个新闻应用和一个视频应用。新闻应用中有多个 Activity 用于展示新闻列表、新闻详情等。如果希望在新闻应用中某个特定的 Activity(比如一个专题新闻 Activity)能够在用户切换任务时,与视频应用的任务栈关联起来,就可以通过设置 TaskAffinity 来实现。这样,当用户从新闻应用切换到视频应用,然后再通过系统的任务切换功能回到这个专题新闻 Activity 时,它会出现在视频应用的任务栈环境中,而不是新闻应用原本的任务栈。

另外,TaskAffinity 在处理启动模式为 singleTask 或 singleInstance 的 Activity 时也很重要。对于 singleTask 的 Activity,系统会根据 TaskAffinity 来查找是否已经存在一个任务栈中有该 Activity 的实例。如果存在,就会将这个任务栈切换到前台,并清除该 Activity 之上的其他 Activity。对于 singleInstance 的 Activity,它会始终在自己独立的任务栈中,这个任务栈的 TaskAffinity 就是该 Activity 所指定的。这有助于更好地管理 Activity 的任务栈结构,提供更灵活的用户体验,比如在不同应用之间共享 Activity 或者在多任务环境下优化 Activity 的展示和切换。

比较一下 ListView 和 RecyclerView,RecyclerView 底层如何实现 Item 复用?两个 RecyclerView 嵌套会有什么问题?

ListView 和 RecyclerView 的比较

ListView 是 Android 早期就存在的用于展示列表数据的视图。它的优点在于使用简单,对于简单的、数据量不大的列表展示场景很适用。例如,一个简单的联系人列表,每个联系人项包含姓名和电话,使用 ListView 可以快速实现。ListView 的一个主要缺点是扩展性相对较差。它的布局管理和视图复用机制相对固定,在处理复杂的列表布局,如包含多种类型的视图或者需要频繁更新数据的场景时,会比较吃力。

RecyclerView 是后来引入的一个更强大的用于展示列表数据的视图。它的灵活性很高。在布局管理方面,RecyclerView 可以通过设置不同的 LayoutManager 来实现线性布局(LinearLayoutManager)、网格布局(GridLayoutManager)、瀑布流布局(StaggeredGridLayoutManager)等多种布局方式。例如,在一个电商应用中,可以使用 RecyclerView 的网格布局来展示商品列表,同时通过设置 StaggeredGridLayoutManager 实现瀑布流布局来展示用户评价中的图片列表。

在数据更新方面,RecyclerView 提供了更高效的更新机制。它可以局部更新数据,减少不必要的视图重绘。例如,当列表中有一个数据项发生变化,RecyclerView 可以只更新这个数据项对应的视图,而不是像 ListView 那样可能会导致整个列表的重绘。

RecyclerView 底层的 Item 复用机制

RecyclerView 的 Item 复用是通过其内部的回收池(Recycler)来实现的。当一个视图(ItemView)滚动出屏幕时,RecyclerView 会将这个视图放入回收池中。回收池是一个按照视图类型进行分类管理的结构。当需要新的视图来填充即将进入屏幕的位置时,RecyclerView 会首先从回收池中查找是否有可以复用的同类型视图。

如果有合适的复用视图,RecyclerView 会将其取出,然后通过 Adapter 的 onBindViewHolder 方法对这个复用的视图进行数据绑定,更新视图显示的内容。例如,在一个聊天消息列表中,当一条消息对应的视图滚动出屏幕后,这个视图会被回收池回收。当新的消息需要显示时,如果回收池中存在合适的消息视图,就会被复用,然后根据新消息的内容进行数据绑定,如更新消息文本、发送时间等显示内容。

两个 RecyclerView 嵌套会出现的问题

当两个 RecyclerView 嵌套时,最常见的问题是滚动冲突。因为内部的 RecyclerView 和外部的 RecyclerView 都有自己的滚动事件处理机制。例如,在一个包含分类列表和每个分类下商品列表的界面中,如果外层的 RecyclerView 用于展示分类,内层的 RecyclerView 用于展示每个分类下的商品,当用户在内部的商品列表 RecyclerView 中进行滚动操作时,可能会意外触发外部分类列表 RecyclerView 的滚动,或者导致滚动不流畅。

另外,性能问题也可能出现。嵌套的 RecyclerView 会增加视图层次结构的复杂性,导致渲染性能下降。尤其是当数据量较大,需要频繁更新数据或者进行滚动操作时,会消耗更多的系统资源,容易出现卡顿现象。而且,在嵌套的情况下,Item 的测量和布局计算也会变得更加复杂,可能会出现布局错乱的情况,例如,内部 RecyclerView 的 Item 宽度或高度计算错误,导致显示不完整或者重叠。

讲讲 Activity 和 Service 的通信方式。

Activity 和 Service 之间有多种通信方式。

通过绑定服务(Bound Service)通信

当 Activity 绑定一个 Service 时,首先要在 Service 中定义一个继承自 Binder 的内部类。这个 Binder 类可以包含一些公共的方法,用于提供 Service 的功能。例如,在一个音乐播放服务中,Binder 类可以包含播放、暂停、获取当前播放进度等方法。

在 Service 的 onBind 方法中,返回这个 Binder 对象。在 Activity 中,通过 bindService 方法来绑定 Service,并在 ServiceConnection 的回调方法 onServiceConnected 中获取这个 Binder 对象。之后,Activity 就可以通过这个 Binder 对象调用 Service 中的方法来进行通信。比如,在音乐播放应用中,Activity 中的播放按钮的点击事件可以调用 Service 的 Binder 对象中的播放方法,从而控制音乐的播放。

这种方式的优点是通信是实时的,Activity 可以随时获取 Service 的状态并进行操作。而且服务的生命周期与绑定的 Activity 紧密相关,当所有绑定的 Activity 都解除绑定后,服务会停止,这使得资源管理更加合理。

通过广播(Broadcast)通信

Service 可以发送广播,Activity 可以注册 Broadcast Receiver 来接收广播。例如,在一个文件下载服务中,当文件下载完成后,Service 可以发送一个自定义广播,广播中可以包含下载文件的相关信息,如文件名、文件存储路径等。

在 Activity 中,通过注册一个 Broadcast Receiver 来接收这个广播。当接收到广播后,Activity 可以根据广播中的信息进行相应的操作,如显示下载完成的提示信息,或者打开下载完成的文件。同样,Activity 也可以发送广播,Service 接收广播后进行相应操作。不过这种方式相对比较松散,因为广播是一种全局的消息传递机制,可能会有其他组件也接收到广播并进行处理,所以需要注意广播的 action 命名等,避免冲突。

通过共享文件或数据库通信

如果 Activity 和 Service 需要共享一些数据,可以通过共享文件或者数据库来实现。例如,在一个数据采集服务中,Service 可以将采集到的数据存储到一个 SQLite 数据库中。Activity 可以通过 ContentResolver 来访问这个数据库,获取数据并进行展示。或者通过共享文件,Service 将数据写入文件,Activity 读取文件内容来获取数据。不过这种方式需要注意数据的同步和权限问题,确保 Activity 和 Service 在访问文件或数据库时有正确的读写权限,并且在数据更新时能够及时获取最新的数据。

Android 生命周期相关,如 onPause 和 onStop 具体是怎么调用的?调用一个透明的 activity,其生命周期怎么变化?屏幕转置时生命周期怎么调用?

onPause 和 onStop 的调用情况

onPause 方法在 Activity 失去焦点但仍然部分可见时被调用。例如,当一个新的 Activity 启动并覆盖在当前 Activity 之上,但当前 Activity 仍然部分可见(如采用了透明主题的新 Activity),或者当一个对话框(Dialog)弹出时,当前 Activity 就会调用 onPause 方法。在这个阶段,Activity 应该暂停一些不关键的操作,如暂停动画播放、停止一些传感器的监听(如果不是必须在后台持续监听的话)。

onStop 方法在 Activity 完全不可见时被调用。比如,当一个新的 Activity 完全覆盖了当前 Activity,或者用户切换到了其他应用,当前 Activity 就会进入 onStop 状态。在这个阶段,Activity 可以释放一些比较占用资源的对象,如关闭数据库连接、停止网络请求等,但是要注意如果 Activity 可能会很快重新回到前台,一些状态数据应该妥善保存,以便能够快速恢复。

透明 Activity 的生命周期变化

当启动一个透明的 Activity 时,被覆盖的 Activity 的 onPause 方法会被调用,因为透明 Activity 覆盖后,原 Activity 失去了焦点。但是 onStop 方法不会被调用,因为原 Activity 仍然是部分可见的。对于透明 Activity 自身,它的生命周期和普通 Activity 类似,会依次经过 onCreate、onStart 和 onResume 等阶段。当透明 Activity 被关闭时,被覆盖的 Activity 会从 onPause 状态恢复,会调用 onResume 方法重新获得焦点。

屏幕转置时生命周期的调用

当屏幕发生转置时,默认情况下,Activity 会被销毁并重新创建。首先会调用 onPause 方法,然后是 onStop 方法,接着 Activity 会被销毁,会调用 onDestroy 方法。之后系统会重新创建 Activity,依次调用 onCreate、onStart 和 onResume 方法。

不过,可以通过在 AndroidManifest.xml 文件中为 Activity 设置 configChanges 属性来改变这种默认行为。例如,设置 “android:configChanges="orientation|screenSize"”,当屏幕转置时,Activity 不会被销毁和重新创建,而是会调用 onConfigurationChanged 方法。在这个方法中,可以手动处理屏幕方向变化带来的布局等相关变化,比如重新加载不同方向的布局资源,调整视图的大小和位置等。

说一下 Broadcast 的种类、注册的方式,以及不同注册方式的生命周期。

Broadcast 的种类

** 系统广播**:这是由 Android 系统发出的广播,用于通知系统中发生的各种事件。例如,当系统开机完成时,会发送 ACTION_BOOT_COMPLETED 广播;当电池电量发生变化时,会发送 ACTION_BATTERY_CHANGED 广播;当网络连接状态改变时,会发送 ACTION_NETWORK_STATE_CHANGED 广播等。这些广播可以让应用及时了解系统状态,做出相应的反应。比如,一个自动更新应用可以在接收到开机广播后,自动检查并下载应用更新。 ** 自定义广播**:这是由应用开发者自己定义的广播。应用可以根据自身的业务需求,发送和接收自定义广播来实现内部模块之间或者不同应用之间的通信。例如,在一个大型的电商应用中,当购物车中的商品数量发生变化时,可以发送一个自定义广播,其他模块(如商品总价计算模块、商品库存检查模块)接收到这个广播后,可以进行相应的操作。

Broadcast 的注册方式

** 静态注册**:在 AndroidManifest.xml 文件中进行注册。通过<receiver>标签来定义 Broadcast Receiver。例如:

<receiver android:name=".MyBroadcastReceiver">
    <intent - filter>
        <action android:name="com.example.myapp.MY_CUSTOM_ACTION" />
    </intent - filter>
</receiver>

在这个例子中,定义了一个名为 MyBroadcastReceiver 的 Broadcast Receiver,并且通过 <intent - filter> 指定了它要接收的广播动作(action)。这种注册方式的优点是 Broadcast Receiver 可以在应用未启动的情况下接收广播。例如,接收开机广播的 Broadcast Receiver 就可以通过静态注册,在系统开机时自动启动应用并接收广播。

** 动态注册**:在代码中通过 registerReceiver 方法进行注册。例如:

BroadcastReceiver myReceiver = new MyBroadcastReceiver();
IntentFilter intentFilter = new IntentFilter("com.example.myapp.MY_CUSTOM_ACTION");
registerReceiver(myReceiver, intentFilter);

这种方式是在运行时进行注册。它的灵活性更高,可以根据应用的运行状态和业务需求来决定何时注册和注销 Broadcast Receiver。例如,在一个只在特定界面需要接收广播的场景下,可以在界面的 onResume 方法中进行动态注册,在 onPause 方法中进行注销。

不同注册方式的生命周期

** 静态注册的 Broadcast Receiver 生命周期**:当应用安装时,系统会根据 AndroidManifest.xml 中的配置信息知道有这个 Broadcast Receiver 的存在。它的生命周期和应用的生命周期是相对独立的。只要系统发出的广播符合它的接收条件,它就可以接收到广播并进行处理。即使应用没有启动,也可以接收广播。例如,接收短信广播的 Broadcast Receiver,如果是静态注册的,当有新短信到达时,就可以接收到广播并进行处理,不过这种方式需要注意权限问题,因为接收一些敏感信息的广播(如短信广播)可能需要相应的权限。 ** 动态注册的 Broadcast Receiver 生命周期**:它的生命周期和注册它的组件(如 Activity)的生命周期紧密相关。当通过 registerReceiver 方法注册后,它就可以接收广播。当组件(如 Activity)被销毁(如调用 onDestroy 方法)时,如果没有注销 Broadcast Receiver,可能会导致内存泄漏。所以通常会在组件的 onPause 或者 onDestroy 方法中注销 Broadcast Receiver。例如,在一个 Activity 中动态注册了一个 Broadcast Receiver 来接收网络状态变化广播,当 Activity 被销毁时,如果不注销这个 Broadcast Receiver,下次系统发送网络状态变化广播时,这个已经不存在的 Activity 相关的 Broadcast Receiver 可能还会尝试接收广播,导致程序出错。

局部广播和全局广播的区别分别是用什么实现的?

局部广播是指在应用内部进行传递的广播,它的作用范围相对较小。主要用于应用内部不同组件之间的通信,例如一个 Activity 和一个 Service 在同一个应用内通过广播进行消息传递。

局部广播是通过 LocalBroadcastManager 来实现的。LocalBroadcastManager 提供了一种高效且安全的广播机制,因为它的广播消息不会离开应用本身,这样可以避免广播被其他应用接收,从而提高了安全性和效率。使用时,需要先获取 LocalBroadcastManager 的实例,通常是通过 LocalBroadcastManager.getInstance (context) 来获取,其中 context 是应用的上下文。然后通过定义 Intent 和对应的 BroadcastReceiver 来发送和接收广播。当发送广播时,使用 LocalBroadcastManager 的 sendBroadcast 方法,接收广播则是通过注册 BroadcastReceiver 来实现,并且这个注册过程也是通过 LocalBroadcastManager 的 registerReceiver 方法完成,和全局广播在 AndroidManifest.xml 文件中静态注册或者在代码中动态注册有所不同。

全局广播则可以在整个系统范围内传递,系统广播就是典型的全局广播。例如系统开机广播、电池电量变化广播等,这些广播可以被系统中任何注册了相应接收动作的应用所接收。同时,应用自己也可以发送全局自定义广播,不过这种广播如果权限设置不当,可能会被其他应用接收并处理。

全局广播主要是通过 Intent 和 BroadcastReceiver 来实现。在发送广播时,使用 sendBroadcast 或者 sendOrderedBroadcast 等方法,这些方法发送的 Intent 可以携带广播动作(action)、数据等信息。在接收广播方面,可以通过在 AndroidManifest.xml 文件中静态注册 BroadcastReceiver,也可以在代码中动态注册。静态注册是在应用安装时系统就知道该应用有这个 BroadcastReceiver 可以接收某些广播,动态注册则是在运行时根据需要进行注册,灵活性更高,但需要注意注销,避免内存泄漏。

由 Android 的 Binder 聊到 IPC 通信,Android 中进程间通信的方式有哪些?为什么是基于 Binder 的,不用传统的操作系统进程间通信方式呢?一个 app 可以开启多个进程吗?怎么做呢?每个进程都是在独立的虚拟机上吗?

Android 中进程间通信(IPC)的方式

** Binder 机制**:这是 Android 中最常用的 IPC 方式。Binder 是一种基于 C/S 架构的通信机制。在 Android 系统中,一个进程可以通过实现一个 Binder 服务端,将自己的功能以接口的形式暴露给其他进程。其他进程作为客户端,可以通过获取服务端的 Binder 对象,然后调用其接口方法来实现通信。例如,一个应用中的 Service 可以通过 Binder 机制与另一个应用进行通信,如提供数据查询服务。 ** Socket 通信**:这是一种比较通用的网络通信方式,在 Android 中也可以用于进程间通信。通过创建 Socket 连接,不同进程可以像在网络中不同主机之间通信一样进行数据传输。不过这种方式相对复杂,需要处理网络协议等相关内容。例如,在一些跨设备通信或者需要高性能网络通信的场景下可以使用 Socket 通信。 ** ContentProvider**:主要用于在不同应用之间共享数据。它通过定义统一的接口,允许其他应用使用 ContentResolver 来访问其管理的数据。例如,一个应用的联系人数据可以通过 ContentProvider 提供给其他应用使用。

为什么 Android 主要基于 Binder 进行 IPC 而不是传统方式

Binder 机制有很多优点。首先,Binder 在安全性方面表现出色。它采用了实名 Binder 和匿名 Binder 的方式,对通信双方进行身份验证,确保只有授权的进程能够进行通信。其次,Binder 的性能较好。它在内存拷贝等方面进行了优化,相比于传统的进程间通信方式(如管道、消息队列等),减少了不必要的开销。另外,Binder 的使用相对简单,对于 Android 开发者来说,更容易理解和实现。它基于面向对象的思想,将服务端的功能以接口的形式提供给客户端,符合 Android 开发的模式。

一个 app 可以开启多个进程吗?怎么做呢?

一个 app 可以开启多个进程。在 AndroidManifest.xml 文件中,可以通过在组件(如 Activity、Service、Receiver 等)的 android:process 属性中指定不同的进程名来开启多个进程。例如,如果有一个 Activity 想要在一个新的进程中运行,可以在 AndroidManifest.xml 文件中这样设置:

<activity
    android:name=".MyActivity"
    android:process=":newProcessName" />

其中,“:newProcessName” 表示这是一个私有进程,它的名字是在应用的包名基础上加上 “:newProcessName”。如果想要创建一个全局进程,可以使用完整的进程名,如 “com.example.myapp.newProcessName”。

每个进程都是在独立的虚拟机上吗?

在早期的 Android 版本中,每个进程都有自己独立的 Dalvik 虚拟机。但是在 Android 5.0 及以后,采用了 ART(Android Runtime)虚拟机,并且引入了 zygote 进程共享机制。在这种机制下,并不是每个进程都有完全独立的虚拟机。当一个新进程被创建时,它会从 zygote 进程中 fork 出来,共享 zygote 进程的一些资源和代码,这样可以提高应用的启动速度和性能。不过,每个进程在内存等资源的使用上还是相对独立的,有自己独立的内存空间来存储数据和运行代码。

内核态与用户态是什么?他们有什么区别?

内核态和用户态是操作系统中两种不同的运行状态。

用户态是应用程序运行的状态。在这个状态下,应用程序只能访问自己的内存空间和一些受限制的系统资源。例如,普通的 Android 应用在用户态运行,它们可以访问自己的文件、数据库等资源,但是对于一些底层的硬件设备(如 CPU、内存管理单元等)的操作是受到限制的。用户态的程序代码运行在较低的特权级别,这样做是为了保证系统的安全性。如果应用程序可以随意访问和修改底层硬件资源,可能会导致系统崩溃或者数据泄露等问题。

内核态则是操作系统内核运行的状态。内核是操作系统的核心部分,它负责管理系统的各种资源,包括硬件资源(如 CPU 调度、内存管理、设备驱动等)和软件资源(如进程管理、文件系统管理等)。在内核态下,程序拥有最高的特权级别,可以访问和操作系统的所有资源。例如,当应用程序需要进行网络通信时,它需要向内核发送请求,内核通过驱动程序来操作网络设备,完成通信任务。

它们之间的主要区别在于访问权限和功能范围。用户态的权限受到限制,主要用于运行应用程序,实现应用的各种功能。而内核态具有最高的权限,用于管理整个系统的资源和操作。从性能角度看,从用户态切换到内核态是有一定开销的。因为这个过程涉及到系统模式的切换、堆栈的切换等操作。例如,当应用程序进行系统调用(如打开一个文件)时,会从用户态切换到内核态,完成操作后再切换回用户态。

产生死锁需要哪些条件?

产生死锁需要四个条件,分别是互斥条件、请求和保持条件、不可剥夺条件和循环等待条件。

互斥条件是指资源在某一时刻只能被一个进程使用。例如,在一个打印机资源的场景中,打印机在同一时刻只能被一个进程用来打印文件,不能多个进程同时使用打印机进行打印。这种互斥性是很多资源的基本属性,因为如果多个进程可以同时对一个资源进行操作,可能会导致数据混乱等问题。

请求和保持条件是指进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。比如,有两个进程 A 和 B,进程 A 已经获得了资源 R1,现在它又请求资源 R2,而资源 R2 被进程 B 占有。同时,进程 A 不会释放资源 R1,这就满足了请求和保持条件。

不可剥夺条件是指进程所获得的资源在未使用完之前,不能被其他进程强行剥夺,只能由获得该资源的进程自己来释放。例如,一个进程获得了一个数据库的写入权限,在它完成写入操作之前,其他进程不能强行剥夺这个写入权限。

循环等待条件是指存在一组进程,每个进程都在等待一个被其他进程占用的资源,并且形成了一个循环等待的链。例如,有三个进程 P1、P2 和 P3,P1 等待 P2 占用的资源 R2,P2 等待 P3 占用的资源 R3,P3 等待 P1 占用的资源 R1,这样就形成了一个循环等待的情况,这是产生死锁的一个关键条件。

Android 热修复相关知识。

Android 热修复是一种在应用已经发布后,能够在不重新发布整个应用的情况下,修复应用中出现的问题(如 Bug)的技术。

热修复主要有几种实现方式。一种是基于类加载机制的热修复,例如使用 DexClassLoader。DexClassLoader 可以加载外部的 dex 文件,通过替换有问题的类来实现修复。在应用运行过程中,如果发现某个类存在问题,将包含修复后类的 dex 文件通过 DexClassLoader 加载到应用中,替换原来有问题的类。不过这种方式需要注意类的加载顺序和兼容性等问题。

还有一种是基于 Java 的反射机制来实现热修复。通过反射可以在运行时修改类的属性和方法。例如,对于一个存在 Bug 的方法,可以在运行时通过反射获取该方法,然后用新的代码逻辑来替换原来的方法。但这种方式相对复杂,并且反射操作如果使用不当可能会影响应用的性能和稳定性。

热修复的优势很明显。它可以快速修复应用中的问题,减少用户因为应用 Bug 而产生的不良体验。例如,一个电商应用在购物高峰期出现了支付流程的 Bug,通过热修复可以在短时间内修复这个问题,避免用户因为无法正常支付而流失。同时,热修复也减少了重新发布应用的成本,不需要经过完整的应用发布流程(如提交应用商店审核等)。

然而,热修复也有一些挑战和限制。从技术角度看,热修复需要考虑不同 Android 版本和设备的兼容性。不同的 Android 设备可能有不同的系统特性和硬件环境,这可能会影响热修复的效果。而且,热修复的安全性也是一个重要问题。如果热修复的代码没有经过严格的审核和测试,可能会引入新的安全隐患,如恶意代码的注入等。在应用开发和维护过程中,需要权衡热修复的必要性和风险,合理使用热修复技术。

用 MultiDex 解决何事?其根本原因在于?Dex 如何优化?主 Dex 放哪些东西?主 Dex 和其他 Dex 调用、关联?Odex 优化点在于什么?

MultiDex 的用途和根本原因

MultiDex 主要用于解决 Android 应用方法数超限制的问题。在 Android 系统中,Dalvik 和 ART 虚拟机在执行应用程序时,会将 dex 文件(Android 字节码文件)加载到内存中。然而,单个 dex 文件的方法数是有上限的(65536 个),随着应用功能的不断增加和引入的库越来越多,很容易超出这个限制。MultiDex 允许应用将代码分割到多个 dex 文件中,从而避免方法数超限导致的编译错误。

Dex 优化

在 dex 优化方面,一种常见的优化是减少 dex 文件的大小。可以通过代码混淆来实现,删除未使用的代码、资源和元数据,从而减小 dex 文件的体积。另外,优化字节码指令也很重要。例如,对一些频繁执行的指令进行优化,提高执行效率。同时,合理安排类和方法的顺序,减少类和方法的引用跨度,也有助于提高加载和执行的速度。

主 Dex 内容

主 Dex 通常放置启动应用所必需的类和方法。比如,应用的入口 Activity 类、Application 类以及一些重要的基础框架类。这些类是保证应用能够正常启动和运行最初阶段所必需的。如果这些关键类被错误地放置到其他 Dex 文件中,可能会导致应用启动失败,如出现 “ClassNotFoundException” 等问题。

主 Dex 和其他 Dex 的调用与关联

主 Dex 和其他 Dex 之间的调用是通过类加载器(ClassLoader)来实现的。当应用需要调用其他 Dex 文件中的类时,类加载器会根据类的全限定名去相应的 Dex 文件中查找并加载。在运行时,它们之间的关联就像一个整体的代码库,只是被分割到了不同的物理文件中。例如,主 Dex 中的一个类可能会实例化其他 Dex 中的类,这个过程是透明的,只要类加载器正确配置,应用就能正常运行。

Odex 优化点

Odex(Optimized Dex)主要的优化点在于提前编译。它将 dex 文件中的字节码在安装时就进行部分编译,生成更接近机器码的形式,这样在应用运行时就可以减少即时编译(JIT)或者提前编译(AOT)的工作量。通过 Odex 优化,可以加快应用的启动速度和执行效率。它还可以对 dex 文件中的常量池进行优化,减少内存占用。例如,对重复的常量进行合并处理,提高内存的利用率。

Dalvik 和 Art 虚拟机区别?

Dalvik 和 ART 虚拟机在多个方面存在区别。

从字节码执行方式来看,Dalvik 是一种基于寄存器的虚拟机。在执行字节码时,它使用寄存器来存储操作数和中间结果。而 ART 虚拟机是基于栈的虚拟机,它在执行字节码时使用操作数栈来完成操作。这种底层执行方式的差异导致了它们在性能和效率上的不同表现。

在编译机制方面,Dalvik 采用即时编译(JIT)技术。这意味着字节码在运行时才会被编译成机器码,这种方式在应用首次运行时会有一定的编译开销,可能导致应用启动稍慢。但是随着应用的运行,频繁执行的代码会被逐渐编译,后续执行效率会有所提高。ART 虚拟机则采用提前编译(AOT)技术,在应用安装时就将字节码编译成机器码。这样的好处是应用启动速度快,因为不需要在运行时进行大量的编译工作。不过,AOT 编译后的文件体积可能会比较大,占用更多的存储空间。

从内存管理角度,ART 虚拟机相对 Dalvik 虚拟机在内存占用和垃圾回收方面有一些优化。ART 的垃圾回收机制更加高效,它可以更准确地回收不再使用的内存,减少内存泄漏的风险。并且由于 AOT 编译的特性,在运行时内存中不需要存储大量的字节码,减少了内存的占用。

另外,在兼容性方面,Dalvik 虚拟机在早期 Android 版本中广泛应用,一些旧的应用可能是基于 Dalvik 开发的。ART 虚拟机在 Android 5.0 及以后版本中逐渐成为主流,它对新的 Android 特性和开发技术有更好的支持,不过也需要开发者对应用进行适当的适配,以充分利用 ART 的优势。

多渠道打包如何实现 (Flavor、Dimension 应用)?从母包生出渠道包实现方法?渠道标识替换原理?

多渠道打包的实现(Flavor 和 Dimension)

在 Android 开发中,使用 Flavor(产品风味)和 Dimension(维度)可以实现多渠道打包。Flavor 主要用于定义不同版本的产品特性。例如,可以定义一个免费版 Flavor 和一个付费版 Flavor,它们在功能、资源等方面可能有所不同。通过在 build.gradle 文件中配置不同的 Flavor,可以为每个 Flavor 设置不同的代码、资源和依赖库。

Dimension 则是用于对 Flavor 进行分类的概念。例如,可以定义一个 “version” 维度和一个 “channel” 维度。在 “channel” 维度下,可以有 “googlePlay”、“huaweiAppGallery” 等不同的渠道 Flavor。在 build.gradle 文件中,先定义 Dimension,然后将 Flavor 分配到不同的 Dimension 中。这样,在打包时,可以根据不同的组合生成多个渠道包。

从母包生出渠道包实现方法

一种常见的方法是在打包过程中通过脚本或者 Gradle 配置来修改渠道标识相关的资源文件或者代码。例如,在资源文件中定义一个字符串用于表示渠道标识。在打包时,根据不同的渠道配置,使用 Gradle 脚本替换这个字符串的值。同时,也可以在代码中通过读取这个渠道标识来进行一些渠道相关的操作,如发送不同渠道的统计信息等。

还可以通过在 AndroidManifest.xml 文件中进行配置来生成渠道包。可以使用占位符的方式,在打包时替换占位符为实际的渠道标识。这样,不同渠道的应用在安装后可以通过读取 AndroidManifest.xml 中的渠道标识来区分自己所属的渠道。

渠道标识替换原理

渠道标识替换主要基于文本替换或者资源替换的原理。在文本替换方面,如前面提到的在代码或者配置文件中定义渠道标识的变量或者字符串。在打包过程中,Gradle 或者打包脚本可以根据渠道配置,将这个变量或者字符串替换为实际的渠道名称。在资源替换方面,Android 的资源管理系统允许在打包时根据不同的条件选择不同的资源。例如,对于不同渠道的图标或者启动画面,可以将它们作为不同的资源文件,在打包时根据渠道选择合适的资源进行打包。这样,每个渠道包在安装后就会显示相应渠道的图标和启动画面,并且通过读取替换后的渠道标识来进行渠道相关的操作。

Android 打包哪些类型文件不能混淆?

在 Android 打包时,有一些类型的文件是不建议或者不能混淆的。

首先是反射相关的类和方法。如果混淆了这些内容,可能会导致反射机制无法正确找到对应的类或者方法。例如,在使用 Java 反射来动态加载类或者调用方法的场景中,混淆后的类名和方法名会发生改变,使得反射代码无法正常工作。

其次是序列化相关的类。当类实现了 Serializable 接口或者 Parcelable 接口用于序列化和反序列化时,混淆可能会破坏序列化的过程。因为序列化是根据类的成员变量的名称和类型来进行的,混淆可能会改变成员变量的名称,导致反序列化时无法正确恢复对象的状态。

与 Android 系统组件交互的类也不应该混淆。例如,Activity、Service、Broadcast Receiver 和 Content Provider 等组件相关的类。这些类通常是由系统根据它们的名称和配置来进行管理和调用的。如果混淆了这些类的名称,可能会导致系统无法正确启动或者调用这些组件,从而导致应用无法正常运行。

另外,一些包含本地方法(Native Method)的类也需要谨慎处理。本地方法是通过 JNI(Java Native Interface)调用的 C 或者 C++ 代码。如果混淆导致本地方法的签名或者名称发生改变,可能会使 JNI 调用无法正确进行,因为 JNI 是通过方法签名来识别和调用本地方法的。

还有用于资源访问的类,如 R 类。R 类是 Android 自动生成的用于资源访问的类,它包含了应用中所有资源(如图标、布局、字符串等)的引用。如果混淆 R 类,可能会导致资源无法正确访问,因为混淆后的 R 类中的资源引用可能会发生错误。

Retrofit 主要实现机制?Retrofit 的作用、原理。

Retrofit 的作用

Retrofit 是一个用于 Android 和 Java 开发中的网络请求库。它的主要作用是简化网络请求的过程。在没有 Retrofit 的情况下,开发者需要手动处理网络连接、请求参数设置、数据解析等一系列复杂的操作。而 Retrofit 提供了一种简洁、高效的方式来进行网络请求,使得开发者可以更加专注于业务逻辑。例如,在一个新闻应用中,通过 Retrofit 可以轻松地从新闻 API 获取新闻列表数据,然后将数据展示给用户。

Retrofit 的原理

Retrofit 的核心原理是基于接口的动态代理。首先,开发者需要定义一个接口,这个接口中的方法代表了不同的网络请求。例如:

public interface NewsApiService {
    @GET("news")
    Call<NewsList> getNews();
}

在这个接口中,@GET 注解表示这是一个 GET 请求,“news” 是请求的路径,Call<NewsList>是返回值类型,其中 NewsList 是自定义的数据结构,用于接收返回的新闻列表数据。

当 Retrofit 创建这个接口的实例时,它实际上是通过动态代理来生成一个代理对象。这个代理对象会拦截接口方法的调用,并将其转换为一个网络请求。在内部,Retrofit 会根据接口中的注解(如 @GET、@POST 等)和方法参数来构建一个实际的网络请求。它会将这个请求交给一个合适的网络请求执行器(如 OkHttp)来执行。

在网络请求执行过程中,Retrofit 还可以处理请求和响应的拦截。例如,可以添加请求头、处理错误响应等。当网络请求返回数据后,Retrofit 会根据接口方法的返回值类型(如 Call<NewsList>),使用合适的转换器(如 GsonConverter)将返回的原始数据(如 JSON 格式的数据)转换为定义的目标数据类型(如 NewsList),然后将转换后的结果返回给调用者。通过这种方式,Retrofit 实现了从接口定义到实际网络请求的转换和处理,大大简化了网络请求的操作流程。

动态代理静态代理区别?

静态代理是在编译时期就确定了代理类和被代理类的关系。代理类需要实现与被代理类相同的接口,并且在代理类中持有被代理类的实例。在代理类的方法中,通过调用被代理类的实例方法来实现功能,同时可以在这些方法调用前后添加额外的逻辑。

例如,假设有一个接口叫 Calculator,有一个实现类 CalculatorImpl,要创建静态代理。首先创建代理类 CalculatorProxy,它也实现 Calculator 接口。在 CalculatorProxy 类中,有一个 CalculatorImpl 的实例,在 CalculatorProxy 的方法中,比如 add 方法,先可以进行一些前置操作,如记录日志,然后调用 CalculatorImpl 实例的 add 方法,最后还可以进行后置操作,如统计方法执行时间。

动态代理则是在运行时动态地生成代理类。在 Java 中,主要通过反射机制和 Proxy 类来实现。动态代理不需要像静态代理那样为每个被代理的接口都编写一个代理类,它可以代理多个不同接口的实现类。

当使用动态代理时,需要实现一个 InvocationHandler 接口。在这个接口的 invoke 方法中,可以处理对被代理对象方法的调用。例如,通过 Proxy.newProxyInstance 方法来创建代理对象,这个方法需要传入类加载器、被代理对象的接口数组和 InvocationHandler 的实现。在 invoke 方法中,可以根据方法名和参数等信息来决定如何处理被代理对象的方法调用,如添加权限验证、事务处理等逻辑。

动态代理和静态代理的主要区别在于,静态代理在编译时就确定了代理关系,代码相对比较直观,但如果有多个被代理类或者接口,需要编写多个代理类。而动态代理是在运行时生成代理类,更加灵活,可以代理不同接口的多个实现类,但代码相对复杂,因为涉及到反射等机制。动态代理的维护成本可能较低,因为不需要为每个新的被代理类编写专门的代理类,而静态代理在添加新的被代理类时,可能需要重新编写或修改代理类。

模块化怎么做?怎么设计?接口发现## 暴漏## 怎么做?基于什么基本思想?

模块化的实现方法

在软件开发中,模块化可以通过多种方式实现。对于编程语言来说,如 Java 有模块系统(Java 9 引入)。可以通过在代码中定义模块,每个模块有自己的模块声明文件(module - info.java),在这个文件中明确模块的名称、依赖的其他模块以及对外暴露的包等信息。例如,一个电商应用可以分为用户模块、商品模块、订单模块等,每个模块有自己独立的代码和功能。

在 Android 开发中,可以使用 Android Studio 的模块功能。通过创建不同的模块,如一个基础库模块、一个功能模块和一个主应用模块。基础库模块可以包含一些通用的工具类和资源,功能模块可以实现特定的功能,如地图功能模块、支付功能模块等。这些模块可以通过 Gradle 进行依赖管理。

模块化的设计

设计模块化架构时,首先要进行功能划分。明确各个模块的功能边界,避免功能重叠和职责不清。例如,在一个社交应用中,消息模块负责消息的发送、接收和展示,用户资料模块负责用户信息的管理和展示,它们之间的功能界限要清晰。

模块之间的通信是关键。可以通过定义接口来实现模块间的通信。比如,消息模块要获取用户资料模块中的用户头像,那么用户资料模块可以通过接口暴露出获取头像的方法,消息模块通过调用这个接口来获取头像。同时,要考虑模块的可替换性,比如可以方便地替换支付模块,从一种支付方式替换为另一种支付方式。

接口发现和暴露

接口发现可以通过模块文档或者代码中的注释来实现。在模块的文档中,详细说明模块提供的接口名称、参数、返回值等信息。对于代码中的接口暴露,在 Java 模块系统中,通过 module - info.java 文件来指定对外暴露的包。在 Android 开发中,可以通过在模块的 build.gradle 文件中设置 public 和 private 属性来确定哪些接口和类可以被其他模块访问。

例如,在一个 Android 模块中,如果希望某个类或者接口可以被其他模块访问,可以在 build.gradle 文件中设置为 public,并且在代码中,将接口设计得清晰明了,参数和返回值符合通用的设计原则。

基本思想

模块化的基本思想是高内聚、低耦合。高内聚是指模块内部的元素(如类、方法)紧密相关,共同完成一个特定的功能。例如,在一个文件上传模块中,文件读取、文件格式检查、文件传输等功能都在这个模块内部紧密结合。低耦合是指模块之间的相互依赖程度要低,通过接口进行通信,这样一个模块的改变不会过多地影响其他模块。例如,当用户模块的内部实现发生变化,只要接口不变,订单模块和商品模块等其他模块就不受影响。

MVC、MVP、MVVM 应用和彼此本质区别?

MVC(Model - View - Controller)的应用和特点

MVC 是一种经典的软件架构模式。在 Android 开发中,Model 层主要负责数据的存储和处理,如数据库操作、网络请求获取数据等。例如,在一个新闻应用中,Model 层可以从服务器获取新闻数据,并对数据进行解析和存储。

View 层负责用户界面的展示,包括布局文件和视图控件的显示。例如,新闻应用中的新闻列表视图和新闻详情视图都属于 View 层。

Controller 层起到了连接 Model 和 View 的作用。它接收用户在 View 层的操作,如点击按钮,然后根据操作来调用 Model 层的方法进行数据处理,并将处理后的结果更新到 View 层。例如,当用户点击新闻列表中的某条新闻,Controller 会调用 Model 获取新闻详情,然后将详情数据更新到新闻详情视图。

MVP(Model - View - Presenter)的应用和特点

MVP 模式在 Android 应用中也较为常见。Model 层和 MVC 中的类似,主要负责数据相关的操作。View 层主要是一个抽象的接口,定义了视图应该具有的展示方法,而不是像 MVC 中直接是具体的视图控件。例如,定义一个显示新闻标题的方法接口。

Presenter 层是 MVP 的核心。它负责处理业务逻辑,从 Model 获取数据,然后将数据通过 View 层定义的接口来更新视图。例如,Presenter 从 Model 获取新闻标题数据,然后调用 View 层的显示新闻标题的接口方法来更新视图。和 MVC 不同的是,MVP 的 View 和 Model 没有直接的交互,都是通过 Presenter 来协调。

MVVM(Model - View - ViewModel)的应用和特点

MVVM 模式在现代 Android 开发中应用广泛。Model 层依然是负责数据的存储和处理。View 层是实际的用户界面,和 MVC 中的 View 类似。

ViewModel 层是 MVVM 的关键。它是一种数据绑定的桥梁,通过数据绑定机制,ViewModel 中的数据变化会自动反映到 View 层的展示上,反之亦然。例如,在一个计数器应用中,ViewModel 中有一个计数器变量,当这个变量的值发生变化时,通过数据绑定,View 层的显示数字会自动更新。同时,View 层的用户操作,如点击增加按钮,也会通过数据绑定修改 ViewModel 中的计数器变量。

本质区别

MVC 中 View 和 Model 可以直接交互,Controller 起到协调作用,但这种模式可能导致 View 和 Model 的耦合度相对较高,当 View 或者 Model 发生变化时,可能会相互影响。MVP 通过 Presenter 来隔离 View 和 Model,使得 View 和 Model 的职责更加清晰,降低了耦合度,但是需要开发者手动实现 View 和 Presenter 之间的接口方法来更新视图。MVVM 通过数据绑定机制,进一步简化了视图更新的过程,ViewModel 和 View 之间的关联更加自动化,减少了手动更新视图的代码量,使得代码更加简洁,维护性更好。

Glide 缓存特点,Glide 原理,大图如何压缩?

Glide 缓存特点

Glide 有强大的缓存系统,包括内存缓存和磁盘缓存。内存缓存用于快速获取最近使用过的图片,它可以有效减少频繁的图片加载,提高应用的响应速度。当应用需要再次显示相同的图片时,如果图片在内存缓存中,Glide 可以直接从内存中获取并显示,避免了重新从磁盘或者网络加载的过程。

磁盘缓存主要用于长期存储图片。它可以存储不同分辨率和格式的图片,以适应不同的设备和显示需求。例如,对于一张高分辨率的图片,Glide 可能会在磁盘缓存中保存多种分辨率的版本,当在一个低分辨率设备上显示时,可以直接从磁盘缓存中获取低分辨率版本,节省了内存和加载时间。

Glide 的缓存策略是灵活的。它可以根据缓存键(Cache Key)来判断是否使用缓存。缓存键是由图片的 URL、尺寸、格式等多种因素构成的。如果缓存键相同,并且缓存中的图片符合要求,就可以直接使用缓存。

Glide 原理

Glide 的加载过程主要从加载请求开始。当应用调用 Glide 的加载方法,如 Glide.with (context).load (url).into (imageView),Glide 会首先创建一个加载请求对象,这个对象包含了图片加载的所有信息,如上下文、目标视图(imageView)、图片来源(URL)等。

然后,Glide 会根据加载请求对象进行一系列的操作。首先是查找缓存,如果在内存缓存或者磁盘缓存中找到了合适的图片,就会直接将图片显示到目标视图中。如果没有找到缓存,Glide 会启动一个异步任务来获取图片,这个异步任务可能是从网络下载或者从本地文件系统读取。

在获取图片后,Glide 会对图片进行一系列的处理,如根据目标视图的尺寸进行缩放、裁剪等操作,以确保图片能够正确地显示在目标视图中。最后,将处理后的图片显示到目标视图中,并将图片存储到缓存中,以便后续使用。

大图如何压缩?

Glide 提供了多种方式来压缩大图。一种方式是通过设置图片的尺寸来压缩。在加载图片时,可以通过 override 方法来指定图片的宽度和高度。例如,Glide.with (context).load (url).override (500, 500).into (imageView),这样 Glide 会将图片按照指定的尺寸进行缩放,从而减少图片的数据量。

另外,Glide 还可以通过采样率(Sample Size)来压缩图片。采样率是指每隔几个像素点取一个像素来构建新的图片。例如,设置采样率为 2,就是每隔一个像素点取一个像素,这样可以大大减少图片的像素数量,从而压缩图片。在 Glide 中,可以通过 downsample 方法或者在加载请求中设置相关参数来实现通过采样率压缩图片。同时,Glide 还可以结合图片格式转换来压缩图片,如将高分辨率的 PNG 格式图片转换为 JPEG 格式,JPEG 格式通常具有更好的压缩效果。

开源库源码、Framework 源码了解多少?

对于开源库源码,以 Retrofit 为例。Retrofit 的核心是基于接口的动态代理。它通过动态代理机制拦截用户定义的接口方法调用,将其转换为实际的网络请求。在源码中,当创建一个 Retrofit 实例时,会对用户定义的接口进行解析,包括接口中的注解(如 @GET、@POST 等)和方法参数。

Retrofit 内部有一个 CallAdapter 工厂,用于将返回的网络请求结果转换为用户定义的接口方法中的返回值类型。例如,如果接口方法返回一个 Call<NewsList>,Retrofit 会通过 CallAdapter 将原始的网络响应数据(如 JSON 格式)转换为 NewsList 对象。

同时,Retrofit 还通过 Converter 来进行数据的转换。如果网络请求返回的是 JSON 数据,Retrofit 可以使用 GsonConverter 或者其他合适的 Converter 将 JSON 数据转换为 Java 对象。在源码中,可以看到这些 Converter 和 CallAdapter 的实现细节,以及它们是如何协同工作来完成网络请求和数据转换的。

对于 Framework 源码,以 Android 的 Activity 生命周期为例。在 Android Framework 源码中,Activity 的生命周期方法(如 onCreate、onStart 等)是由系统底层的 ActivityThread 来管理的。当一个 Activity 被启动时,ActivityThread 会执行一系列的操作来创建 Activity 实例,并调用其生命周期方法。

在 onCreate 方法中,系统会加载布局资源,通过 LayoutInflater 将 XML 布局文件转换为视图对象,并添加到 Activity 的视图层次结构中。在源码中可以看到这些操作的详细过程,包括资源的加载、视图的创建和添加等步骤。

另外,在 Android Framework 中,对于广播机制(Broadcast)的源码,系统通过 IntentFilter 来匹配广播消息和接收者。当发送一个广播时,系统会遍历所有注册的 Broadcast Receiver,根据它们的 IntentFilter 来判断是否接收这个广播。在源码中可以看到这个匹配过程的详细实现,包括如何处理不同类型的广播(如系统广播和自定义广播)和不同的注册方式(如静态注册和动态注册)。

安卓中哪些地方用到了观察者模式?

在安卓开发中,观察者模式有多处应用。

首先是广播机制。广播接收者(Broadcast Receiver)就是观察者,而广播发送者是被观察的对象。当发送者发送一个广播(如系统开机广播或者自定义广播),系统会通知所有注册了相应广播动作(action)的接收者。这些接收者可以根据广播中的信息做出相应的操作。例如,一个应用中的多个组件(如 Activity 和 Service)可以注册接收同一个自定义广播,当广播被发送时,这些组件就像观察者一样收到通知并进行数据更新或者其他操作。

其次是内容观察者(ContentObserver)。它用于观察数据的变化,特别是 Content Provider 中的数据。当 Content Provider 中的数据发生变化时,如数据库中的某条记录被更新或者删除,ContentObserver 会收到通知。例如,在联系人应用中,如果联系人数据存储在 Content Provider 中,当有新联系人添加或者现有联系人信息修改时,注册了 ContentObserver 的组件(如另一个显示联系人信息的 Activity)可以及时获取数据更新并刷新界面。

还有 LiveData 也是观察者模式的应用。在 Android 架构组件中,LiveData 是一种可观察的数据持有者。ViewModel 可以持有 LiveData 对象,而 Activity 或者 Fragment 可以观察这个 LiveData。当 LiveData 中的数据发生变化时,观察者(Activity 或者 Fragment)会收到通知并更新界面。例如,在一个天气应用中,ViewModel 通过网络请求获取天气数据并存储在 LiveData 中,当数据更新时,观察这个 LiveData 的界面组件就会自动更新显示。

另外,在一些自定义的组件或者框架中也经常会用到观察者模式。比如,一个自定义的消息推送系统,消息发送者作为被观察对象,多个消息接收者作为观察者,当有新消息时,接收者能够及时得到消息并进行处理。

介绍一下所有的 map,以及## 他们## 之间的对比、适用场景。

在 Java(包括安卓开发)中有多种 Map 类型。

HashMap

HashMap 是最常用的 Map 实现之一。它基于哈希表实现,通过键(key)的哈希值来存储和查找元素。它的优点是插入、删除和查找操作的时间复杂度在理想情况下可以达到常数级别。例如,在一个存储用户信息的场景中,以用户 ID 为键,用户对象为值,使用 HashMap 可以快速地根据用户 ID 获取用户信息。

不过,HashMap 是无序的,即元素的存储顺序和插入顺序没有必然联系。而且,它不是线程安全的,如果在多线程环境下同时对 HashMap 进行修改操作,可能会导致数据不一致或者出现错误。

LinkedHashMap

LinkedHashMap 在 HashMap 的基础上,通过维护一个双向链表来记录元素的插入顺序或者访问顺序。如果按照插入顺序维护,那么遍历 LinkedHashMap 时,元素的顺序就是插入的顺序。这在需要按照插入顺序进行操作的场景下很有用,比如缓存最近访问的文件列表,按照文件访问的顺序存储在 LinkedHashMap 中。

LinkedHashMap 在性能方面和 HashMap 类似,插入、删除和查找操作的效率也比较高,只是由于维护了链表,会有一些额外的开销。它也不是线程安全的。

TreeMap

TreeMap 是基于红黑树实现的有序 Map。它的键是按照自然顺序或者自定义的比较器(Comparator)进行排序的。例如,在一个存储单词及其出现频率的场景中,按照字母顺序存储单词作为键,频率作为值,使用 TreeMap 可以方便地按照字母顺序遍历单词。

TreeMap 的查找、插入和删除操作的时间复杂度是对数级别,相对 HashMap 在某些情况下可能会稍慢一些。它适用于需要对键进行排序的场景,如排序后的统计数据展示。TreeMap 是线程安全的,但是在多线程频繁修改的情况下,性能可能会受到影响。

Hashtable

Hashtable 和 HashMap 类似,也是基于哈希表实现的。但是 Hashtable 是线程安全的,它的所有方法都是同步的。不过,这也导致了它的性能在单线程环境下可能不如 HashMap。在早期的 Java 开发中使用较多,现在由于有更好的线程安全的集合类(如 ConcurrentHashMap)以及性能更好的 HashMap,Hashtable 的使用场景相对较少。它主要用于一些对线程安全要求较高,且对性能要求不是特别极致的场景。

ConcurrentHashMap

ConcurrentHashMap 是为了在多线程环境下高效地使用 Map 而设计的。它采用了分段锁的机制,允许多个线程同时对不同的段进行操作,而不会互相干扰。在高并发的场景下,如多个线程同时对一个共享的缓存 Map 进行读写操作,ConcurrentHashMap 可以提供更好的性能和线程安全性。

Android 11 有什么新的特性?

隐私保护增强

在 Android 11 中,隐私保护是一个重点改进的方面。应用在访问用户的一次性权限时受到更多限制。例如,当应用请求访问用户的位置信息或者摄像头等权限时,用户可以授予一次性权限,下次应用再次需要访问时,需要重新请求。并且,系统会自动重置应用的权限,当应用长时间未使用某个权限相关的功能时,权限会被自动回收。

另外,系统还加强了对后台应用访问用户数据的管控。后台应用在访问用户的敏感信息(如照片、文件等)时,需要经过用户的明确授权,并且系统会对后台应用的行为进行记录和提示,用户可以随时查看哪些后台应用访问了自己的数据。

通信优化

在通信方面,Android 11 对 5G 网络进行了更好的支持。它可以根据 5G 网络的特性,如低延迟、高带宽等,优化应用的网络体验。例如,对于一些对网络要求较高的应用(如高清视频播放、云游戏等),可以更好地利用 5G 网络的优势。

同时,Android 11 还优化了与聊天应用等即时通讯工具相关的功能。例如,通过新的气泡通知(Bubble Notification),聊天消息可以以气泡的形式悬浮在屏幕上,用户可以方便地查看和回复消息,而无需切换应用或者打开通知栏。

屏幕和显示改进

对于屏幕显示,Android 11 支持更高的刷新率。这使得屏幕的显示更加流畅,在滚动列表、播放动画等场景下可以提供更好的视觉体验。例如,一些支持高刷新率的手机可以在 Android 11 下充分发挥其优势,将屏幕刷新率提升到 120Hz 或者更高。

此外,还对屏幕截图和录屏功能进行了优化。用户可以更加方便地进行屏幕截图和录屏操作,并且可以对截图和录屏进行简单的编辑,如添加注释、裁剪等操作。

安卓的新特性。

跨设备互联增强

安卓系统在跨设备互联方面有了新的进展。例如,与智能手表、平板电脑等设备的连接更加紧密。用户可以在手机和平板之间无缝切换应用,共享剪切板内容。当在手机上复制一段文字,在平板上可以直接粘贴,这种跨设备的交互体验更加流畅。

并且,安卓系统与智能家居设备的整合也越来越好。可以通过安卓手机控制智能灯、智能摄像头等多种智能家居设备。例如,在回家的路上,用户可以通过手机提前打开家里的空调,调整灯光亮度等。

系统性能优化

安卓系统不断对性能进行优化。在内存管理方面,通过新的算法来优化应用的内存占用。当应用进入后台时,系统可以更好地回收内存,同时在应用重新回到前台时,能够快速恢复之前的状态。

在存储管理上,对应用的存储权限进行了更加精细的划分。应用在存储文件时,需要明确请求相应的权限,并且系统可以更好地管理应用的存储配额,避免某些应用过度占用存储空间。

人工智能和机器学习集成

安卓系统开始更多地集成人工智能和机器学习技术。例如,在相机应用中,通过机器学习算法来优化拍照效果。可以自动识别场景,如风景、人物、夜景等,然后根据不同的场景调整相机参数,如曝光度、色彩饱和度等,以拍摄出更好的照片。

另外,在系统的智能语音助手方面也有了改进。语音助手可以更好地理解用户的意图,提供更准确的回答和帮助。例如,在查询天气、设置提醒等简单任务之外,还可以帮助用户进行更复杂的操作,如查找文件、安排会议等。

讲一下 Handler,MessageQueue 的排序方式是什么,用 post 和 postDelay,message 加入到 messagequeue 有什么不同?Handler 底层,postDelay 源码。

MessageQueue 的排序方式

MessageQueue 是按照消息(Message)的触发时间来排序的。当消息被发送到 MessageQueue 时,会根据消息的 when 属性(这个属性表示消息应该被处理的时间)来确定在队列中的位置。如果 when 属性的值较小,那么消息会排在队列的前面,更早地被处理。例如,一个立即执行的消息(when 属性为当前时间)会比一个延迟执行的消息(when 属性为未来某个时间)更早地在队列中被处理。

post 和 postDelay 的区别

Handler 的 post 方法是将一个 Runnable 对象发送到 MessageQueue 中,这个消息会尽快被处理。它的原理是将 Runnable 对象包装成一个 Message,然后将 Message 发送到 MessageQueue 中。当消息被处理时,会执行 Runnable 对象中的 run 方法。

而 postDelay 方法则是带有延迟的发送。它同样是将 Runnable 对象包装成 Message,但是会设置 Message 的 when 属性为当前时间加上延迟的时间。这样,这个消息会在延迟时间过后才会被处理。例如,在一个动画效果中,使用 postDelay 可以实现每隔一段时间执行一段动画代码,通过控制延迟时间来调整动画的节奏。

Handler 底层机制

Handler 底层是和 MessageQueue 以及 Looper 紧密相关的。Looper 是一个消息循环,它会不断地从 MessageQueue 中取出消息并进行处理。Handler 则是用于向 MessageQueue 发送消息和处理消息的工具。

当创建一个 Handler 时,它会关联一个 Looper。通常情况下,在主线程中已经有一个默认的 Looper。Handler 通过这个 Looper 将消息发送到 MessageQueue 中。在底层,Handler 会调用 MessageQueue 的相关方法(如 enqueueMessage)来将消息加入队列。

当 Looper 从 MessageQueue 中取出消息时,会根据消息中的目标 Handler 来将消息分发给相应的 Handler 进行处理。Handler 的 dispatchMessage 方法会根据消息的类型来决定如何处理。如果消息是一个 Runnable(通过 post 或者 postDelay 发送的),会执行 Runnable 的 run 方法;如果消息有一个回调(Callback),会先执行回调方法;如果都没有,会执行 Handler 的 handleMessage 方法。

postDelay 源码分析

在 Handler 的 postDelay 方法中,首先会将 Runnable 对象包装成一个 Message,这个过程和 post 方法类似。然后会计算延迟时间后的触发时间(when),通过 SystemClock.uptimeMillis () 获取当前系统的开机时间,再加上延迟时间得到 when 属性的值。

接着,会调用 sendMessageAtTime 方法,这个方法内部会将 Message 发送到 MessageQueue 中。在 MessageQueue 的 enqueueMessage 方法中,会根据消息的 when 属性来将消息插入到合适的位置。如果 when 属性的值小于当前队列头部消息的 when 属性,那么这个消息会被插入到头部,否则会通过遍历队列来找到合适的插入位置,确保 MessageQueue 中的消息是按照触发时间排序的。

Handler 的工作流程,Handler 能否在子线程初始化以及用什么方案来替代 Handler 在子线程初始化?

Handler 工作流程

Handler 的工作流程主要涉及到消息的发送、排队和处理。首先,Handler 通过关联一个 Looper 来与消息队列(MessageQueue)建立联系。通常在主线程中已经有一个默认的 Looper,这个 Looper 会一直循环从 MessageQueue 中取出消息。

当通过 Handler 的 post 方法或者 sendMessage 方法发送消息时,Handler 会将消息(如果是 post 方法发送的 Runnable,会将其包装成消息)放入 MessageQueue 中。MessageQueue 会根据消息的触发时间(when 属性)对消息进行排序。

然后,Looper 不断地从 MessageQueue 中取出消息。当取出一个消息后,Looper 会根据消息中的目标 Handler 将消息分发给对应的 Handler 进行处理。Handler 的 dispatchMessage 方法会根据消息的类型来决定如何处理。如果消息包含一个 Runnable(通过 post 发送的),就执行 Runnable 的 run 方法;如果消息有一个回调(Callback),会先执行回调方法;如果都没有,会执行 Handler 自己的 handleMessage 方法。

Handler 在子线程初始化问题及替代方案

Handler 可以在子线程中初始化,但需要注意的是,子线程默认没有 Looper。如果要在子线程中使用 Handler,需要先为子线程创建一个 Looper。可以通过调用 Looper.prepare () 方法来创建一个 Looper,然后调用 Looper.loop () 方法来启动消息循环。不过这种方式比较复杂,而且如果处理不当可能会导致一些问题。

替代方案是使用 HandlerThread。HandlerThread 是一个自带 Looper 的线程类。当创建一个 HandlerThread 对象并启动它后,就有了一个带有 Looper 的线程。可以通过 HandlerThread 的 getLooper () 方法获取这个 Looper,然后用这个 Looper 来创建 Handler。这样就可以在子线程环境下方便地使用 Handler 来处理消息,而且不需要手动去创建和管理 Looper。

Handler 如何切换线程?Handler 如何回调?Handler 注意事项 (预防内存泄漏),Handler 封装使用 (handlerThread、IntentService)。

Handler 切换线程的方式

Handler 本身不能直接切换线程。但是可以利用它来实现跨线程通信。例如,在子线程中通过 Handler 发送消息,然后在主线程的 Handler 中处理消息,从而实现从子线程到主线程的数据传递。具体操作是,在主线程创建一个 Handler,在子线程通过获取这个 Handler 的引用,使用它来发送消息。当消息在主线程被处理时,就实现了从子线程到主线程的切换。

Handler 回调机制

Handler 的回调主要是通过 dispatchMessage 方法实现。当 Looper 从 MessageQueue 中取出消息并分发给 Handler 后,会调用 Handler 的 dispatchMessage 方法。如果消息是通过 post 方法发送的 Runnable,那么在 dispatchMessage 方法中会执行这个 Runnable 的 run 方法,这就是一种回调。如果消息设置了一个 Callback 对象,那么 dispatchMessage 会先执行 Callback 的 handleMessage 方法作为回调。如果消息没有 Runnable 也没有 Callback,那么会执行 Handler 自身的 handleMessage 方法,开发者可以在这个方法中实现自定义的回调逻辑。

Handler 注意事项(预防内存泄漏)

在使用 Handler 时,要注意预防内存泄漏。如果一个 Handler 是一个内部类,并且它持有外部类的引用(这是很常见的情况),当 Handler 发送的消息还在 MessageQueue 中等待处理,而外部类的实例已经不需要了(如 Activity 被销毁),此时由于 Handler 持有外部类的引用,外部类无法被垃圾回收,就会导致内存泄漏。

为了避免这种情况,可以将 Handler 定义为静态内部类,并且使用 WeakReference 来持有外部类的引用。这样,当外部类的实例被销毁后,由于是弱引用,不会阻止垃圾回收。在 Handler 的 handleMessage 方法中,可以通过弱引用获取外部类的实例,如果实例已经不存在,就不进行操作,从而避免了内存泄漏。

Handler 封装使用(HandlerThread、IntentService)

HandlerThread:如前面提到的,HandlerThread 是一个自带 Looper 的线程。它的使用场景主要是在需要在子线程中处理消息的情况。例如,在一个文件下载任务中,可以创建一个 HandlerThread,然后在这个线程中通过 Handler 来处理下载进度的更新消息。当文件下载进度发生变化时,通过 Handler 发送消息,在 HandlerThread 中的 Handler 处理这些消息,更新下载进度的显示,这样就不会阻塞主线程。

IntentService:IntentService 是一种特殊的 Service,它内部使用了 Handler 和 HandlerThread 来处理异步任务。当通过 startService 方法启动 IntentService 时,它会将传入的 Intent 放入一个工作队列,然后通过 Handler 和 HandlerThread 来依次处理这些 Intent。例如,在一个批量图片处理的应用中,可以通过 startService 方法传入包含图片路径等信息的 Intent,IntentService 会在后台线程(通过 HandlerThread)中处理这些图片,处理完成后自动停止服务。这种方式简化了在 Service 中处理异步任务的过程,并且由于是在后台线程处理,不会阻塞主线程。

为什么主线程 loop 不会 ANR?ThreadLocal 原理。

主线程 loop 不会 ANR 的原因

在安卓中,ANR(Application Not Responding)主要是因为主线程在规定的时间内(如 5 秒内)没有响应输入事件或者 BroadcastReceiver 没有在规定时间(如 10 秒)内执行完 onReceive 方法。

主线程的 Looper.loop () 方法虽然是一个循环,不断地从 MessageQueue 中取出消息并处理,但它会在适当的时候让出 CPU 资源。例如,当 MessageQueue 为空时,Looper 会进入阻塞状态,等待新的消息到来。这样就不会一直占用 CPU,导致其他重要的系统操作(如输入事件处理)无法进行。

另外,系统对输入事件等有专门的处理机制。当有输入事件发生时,系统会将对应的消息放入主线程的 MessageQueue 中,并且这些消息具有较高的优先级。Looper 会及时处理这些消息,从而保证了应用能够及时响应输入事件,避免了 ANR。

ThreadLocal 原理

ThreadLocal 是一个用于在多线程环境下存储线程局部变量的工具。它的主要原理是每个线程都有一个独立的变量副本。

当创建一个 ThreadLocal 对象并设置一个值时,实际上是在当前线程的内部存储区域(可以理解为一个线程内部的 Map)中,以 ThreadLocal 对象为键,存储了这个值。当在同一个线程中获取这个 ThreadLocal 的值时,会从这个线程自己的内部存储区域中取出对应的键值对中的值。

例如,在多个线程使用同一个 Handler 时,为了避免线程安全问题,可以使用 ThreadLocal 来存储每个线程的 Looper。每个线程都有自己的 Looper 副本,通过 ThreadLocal 来获取和设置。这样,当在不同的线程中操作 Handler 时,就不会因为共享 Looper 而出现问题。

launcher 应用抽屉,之前有个毛玻璃效果背景,从系统 ROM 角度说下怎么做?实时的睡眠水面倒影效果怎么做?实时更新的 UI 性能如何保证?

Launcher 应用抽屉毛玻璃效果背景的实现(从系统 ROM 角度)

从系统 ROM 角度实现 launcher 应用抽屉的毛玻璃效果背景,首先需要考虑图形处理的底层支持。可以利用图形库(如 OpenGL)来实现模糊效果。

在获取抽屉背景图像后,通过图形处理算法来对图像进行模糊处理。一种常见的方法是高斯模糊。系统 ROM 可以通过调用底层的图形处理模块,将抽屉背景图像的像素数据传递给高斯模糊算法。这个算法会根据设定的模糊半径等参数,对每个像素及其周围像素进行加权平均计算,从而使图像产生模糊效果。

在实现过程中,还需要考虑性能问题。可以通过降低模糊处理的分辨率来提高处理速度。例如,将原始的高分辨率背景图像先进行下采样,得到一个较低分辨率的图像进行模糊处理,然后再将模糊后的图像上采样回原始分辨率。这样可以在一定程度上减少计算量,同时保持较好的视觉效果。

另外,为了实现毛玻璃效果,可以将模糊后的图像与一个半透明的遮罩层进行叠加。遮罩层可以通过设置透明度来调整毛玻璃效果的强度,使背景图像呈现出模糊且有一定透明度的效果。

实时的睡眠水面倒影效果的实现

实现实时的睡眠水面倒影效果,首先要对水面倒影的物理特性进行建模。水面倒影会随着水面的波动而变化,所以需要模拟水面的波动。

可以使用数学函数来模拟水面的波动,如正弦函数或余弦函数。通过在一定时间间隔内改变函数的参数(如频率、振幅等),来模拟水面的动态变化。例如,在每一帧的绘制中,根据当前的时间和设定的波动参数,计算出水面每个点的位置变化。

对于倒影的绘制,首先要对物体进行垂直翻转,得到倒影的基本形状。然后,根据水面的波动情况,对倒影进行扭曲处理。可以通过对倒影图像的像素坐标进行变换来实现扭曲效果。例如,根据水面波动函数,将倒影图像的每个像素的垂直坐标进行动态调整,使倒影看起来像是随着水面波动。

在实时绘制过程中,为了保证性能,需要优化倒影的绘制区域。只对可见区域内的倒影进行绘制,避免对整个屏幕进行不必要的计算。同时,可以使用纹理映射等技术,将预先计算好的倒影纹理应用到水面上,减少实时计算的工作量。

实时更新的 UI 性能保证

为了保证实时更新 UI 的性能,首先要优化布局。尽量使用简单的布局结构,避免嵌套过多复杂的布局。例如,使用 LinearLayout 而不是复杂的 RelativeLayout,因为 LinearLayout 的布局计算相对简单。

在绘制方面,减少不必要的绘制操作。可以通过设置视图的可见性(setVisibility)来控制视图是否需要绘制。例如,对于一些暂时不需要显示的视图,可以将其设置为不可见,这样在实时更新 UI 时,就不会对这些视图进行绘制。

对于动画效果,采用硬件加速。现代的安卓设备都支持硬件加速,通过开启硬件加速,可以将部分图形绘制和处理任务交给 GPU 来完成,大大提高了动画的绘制速度。例如,对于一些简单的平移、缩放动画,使用硬件加速可以获得更流畅的效果。

另外,在数据更新方面,采用高效的数据更新策略。例如,使用 DiffUtil 来更新 RecyclerView 的数据。DiffUtil 可以计算出数据的变化差异,然后只对有变化的视图进行更新,而不是重新绘制整个 RecyclerView,从而减少了更新的工作量,提高了性能。

UI 基础:Measure、Layout、draw 大流程、绘制顺序,FlowLayout 怎么实现?

UI 的 Measure、Layout、draw 大流程

Measure(测量)流程

测量过程主要是确定视图的大小。对于每个视图,它的父视图会调用它的 measure 方法来开始测量。在 measure 方法中,视图会根据自身的布局参数(如宽高的模式是 match_parent、wrap_content 还是固定尺寸)和子视图的情况来确定自己的大小。

如果是 ViewGroup(视图组),它还需要遍历所有的子视图,调用子视图的 measure 方法来测量子视图的大小。例如,在 LinearLayout 中,会根据子视图的布局参数和方向(水平或垂直)来确定子视图的大小,并且考虑子视图之间的间距等因素。

Layout(布局)流程

布局过程是确定视图的位置。在测量完成后,父视图会调用子视图的 layout 方法来确定子视图的位置。父视图会根据自己的布局规则和子视图的大小,通过设置子视图的四个边界(left、top、right、bottom)来确定子视图在屏幕上的位置。

例如,在 RelativeLayout 中,子视图的位置是根据相对其他子视图或者父视图的位置关系来确定的。子视图可以通过设置属性(如 layout_above、layout_toRightOf 等)来指定自己相对于其他视图的位置。

Draw(绘制)流程

绘制过程是将视图的内容显示在屏幕上。当布局完成后,视图会通过 draw 方法来进行绘制。绘制过程会根据视图的类型和内容来进行不同的操作。

对于简单的视图(如 TextView),会先绘制背景,然后绘制文字内容。对于复杂的视图(如自定义视图),可能会涉及到更复杂的绘制操作,如绘制图形、使用画笔(Paint)来绘制线条等。

绘制顺序通常是从父视图到子视图。首先绘制父视图的背景,然后按照视图层次结构依次绘制子视图,最后绘制父视图的前景(如果有的话)。

绘制顺序

绘制顺序是按照视图层次结构进行的。首先,最顶层的父视图开始绘制,它会先绘制自己的背景。然后,它会遍历自己的子视图,按照添加顺序或者其他布局规则确定的顺序来依次绘制子视图。在子视图绘制过程中,同样会先绘制背景,再绘制内容,最后绘制前景。

当所有子视图绘制完成后,父视图会绘制自己的前景(如果有)。这样层层递归,直到所有的视图都绘制完成,最终呈现出完整的 UI 界面。

FlowLayout 的实现

FlowLayout 是一种布局方式,它的子视图会按照添加的顺序,在一行内排列,当一行排满后,会自动换行。

在实现 FlowLayout 时,首先要继承 ViewGroup。在测量(measure)过程中,需要遍历子视图,计算每一行能够容纳的子视图数量。根据子视图的大小(通过 measure 子视图得到)和布局的可用宽度(考虑到边距等因素),确定每行可以放置多少个子视图。

在布局(layout)过程中,根据测量得到的每行子视图的信息,设置子视图的位置。对于每一行,从左到右依次设置子视图的 left 和 top 边界。当一行排满后,更新下一行的 top 边界,开始新一行的布局。

在绘制(draw)过程中,和其他 ViewGroup 类似,先绘制自己的背景,然后按照布局好的位置依次绘制子视图,最后绘制自己的前景。同时,要考虑到当视图大小发生变化或者有新的子视图添加时,能够重新进行测量、布局和绘制操作,以保证布局的正确性和灵活性。

TCP 和 UDP 的区别以及所属协议层

TCP(传输控制协议)和 UDP(用户数据报协议)都是传输层协议。它们有以下区别:

首先,从可靠性方面来说,TCP 是一种可靠的传输协议。它通过序列号、确认应答、重传机制等来保证数据能够准确无误地从发送端传输到接收端。例如,发送方发送一个数据包后会等待接收方的确认应答,如果在一定时间内没有收到应答,就会重新发送该数据包。而 UDP 是不可靠的传输协议,它不保证数据一定能够完整无误地到达目的地,数据报可能会丢失、重复或者乱序,但是它的传输速度相对较快。

其次,从连接方式来看,TCP 是面向连接的协议。在通信之前,需要通过三次握手来建立连接,通信结束后还需要通过四次挥手来断开连接。就像是打电话,需要先拨通号码建立连接,通话结束后再挂断。UDP 是无连接的协议,发送数据时不需要提前建立连接,就像寄信,把信发出去就可以了,不用管接收方是否准备好了接收。

再者,从数据传输方式看,TCP 是字节流传输,它把应用层交下来的数据看成一连串无结构的字节流,发送端会将字节流分割成合适的报文段发送出去,接收端将收到的报文段重组为字节流交给应用层。UDP 是面向报文的,应用层交给 UDP 多长的报文,UDP 就照样发送,即一次发送一个报文,接收方收到的也是一个报文。

OSI 七层网络模型介绍及各层协议举例和五层模型

OSI(开放系统互连)模型是一个用于计算机或通信系统间互联的标准体系。

第一层是物理层,主要处理物理介质上的信号传输,包括电缆、光纤、无线等传输介质的电气、机械等特性。例如常见的以太网电缆标准(如 10Base - T、100Base - TX 等)就属于物理层协议,它规定了电缆的类型、传输速率、信号的编码方式等。

第二层是数据链路层,主要负责将物理层接收到的信号转换为数据帧,并进行错误检测和纠正,同时处理介质访问控制(MAC)。像以太网协议(IEEE 802.3)就工作在这一层,它定义了数据帧的格式,包括源 MAC 地址、目的 MAC 地址、数据和帧校验序列等部分。交换机是工作在数据链路层的设备,它根据 MAC 地址来转发数据帧。

第三层是网络层,主要功能是进行逻辑寻址和路由选择。IP 协议(Internet Protocol)是网络层最重要的协议之一,它为每个连接到网络的设备分配一个唯一的 IP 地址,通过 IP 地址来确定数据从源端到目的端的路径。路由器是工作在网络层的设备,它根据 IP 地址和路由表来转发数据包,比如一个企业网络中,通过路由器可以将内部网络和外部互联网连接起来,并且根据目的 IP 地址将数据包发送到正确的网络。

第四层是传输层,也就是前面提到的 TCP 和 UDP 所在的层,它主要负责在不同主机的进程之间提供端到端的通信服务。除了 TCP 和 UDP,还有 SCTP(流控制传输协议)等协议也属于传输层,SCTP 主要用于在 IP 网络上传输信令消息,比如在电信网络中用于传输电话呼叫的信令信息。

第五层是会话层,主要负责建立、管理和终止会话。它可以控制会话的建立和拆除,协调会话双方的通信。例如,在一些远程登录协议中,会话层就起到了管理会话的作用,像 NetBIOS 会话服务,它用于在局域网内建立和维护计算机之间的会话,使得不同计算机上的应用程序能够进行通信。

第六层是表示层,主要处理数据的表示、转换和加密等功能。例如,在不同的计算机系统之间进行数据传输时,可能会因为数据格式的不同(如 ASCII 码和 EBCDIC 码)而需要进行转换,这一层就负责进行这样的操作。另外,数据加密和解密也是表示层的功能之一,像 SSL(安全套接层)协议的一部分功能(加密相关部分)就可以看作是在表示层实现的,它可以对应用层的数据进行加密,以保证数据在传输过程中的安全性。

第七层是应用层,它是最接近用户的一层,为用户提供各种网络应用服务。常见的协议有 HTTP(超文本传输协议)用于网页浏览,FTP(文件传输协议)用于文件传输,SMTP(简单邮件传输协议)用于电子邮件的发送等。

计网五层模型是对 OSI 七层模型的简化,它将应用层、表示层、会话层合并为应用层,物理层和数据链路层合并为链路层,加上网络层、传输层和应用层就构成了五层模型。这种模型更符合实际网络通信的实现,因为在实际应用中,有些层次的功能界限并不是那么清晰,五层模型使得网络通信的实现和理解更加简洁明了。

TCP 是怎么保证的可靠传输

TCP 通过多种机制来保证可靠传输。

一是序列号机制。TCP 为每个发送的字节都编上一个序号,这个序号在整个 TCP 连接中是唯一的。接收方可以根据这个序号来判断数据包的顺序是否正确,并且可以识别出是否有重复的数据包。例如,发送方发送了三个数据包,序号分别是 100、200、300,接收方收到数据包后可以根据序号将它们按顺序排列,如果收到序号为 200 的数据包两次,就可以判断出有重复数据包。

二是确认应答机制。接收方在收到数据包后,会向发送方发送一个确认应答(ACK)消息,告诉发送方已经正确收到了数据包。确认应答中包含接收方期望收到的下一个数据包的序号。例如,接收方收到序号为 100 的数据包后,会发送一个 ACK 消息,其中确认号为 200,表示已经收到序号 100 的数据包,并且期望下一个收到序号为 200 的数据包。发送方收到这个 ACK 消息后,就知道前面发送的数据包已经成功到达。

三是重传机制。如果发送方在一定时间内没有收到确认应答,就会认为数据包丢失,然后重新发送该数据包。这个时间是通过往返时间(RTT)估算出来的。例如,发送方发送一个数据包后启动一个定时器,如果定时器超时还没有收到 ACK,就会重传该数据包。同时,TCP 还采用了自适应的重传算法,根据网络的实际情况动态调整重传时间间隔,以提高传输效率。

四是滑动窗口机制。滑动窗口用于控制发送方的发送速率,同时也能保证接收方能够来得及处理收到的数据包。发送方有一个发送窗口,窗口内的数据可以发送,接收方也有一个接收窗口。发送方根据接收方返回的窗口大小来调整自己的发送窗口。例如,接收方的接收窗口大小为 1000 字节,发送方就可以在这个范围内发送数据。当接收方处理完一部分数据后,会更新接收窗口大小并通知发送方,发送方就可以根据新的窗口大小继续发送数据。通过这种方式,既可以避免发送方发送过多数据导致接收方缓冲区溢出,又可以充分利用网络带宽,提高传输效率。

三次握手的过程、四次挥手的过程以及原因

三次握手过程

第一次握手:客户端向服务器发送一个带有 SYN(同步序列号)标志的 TCP 报文段,这个报文段中还包含客户端的初始序列号(ISN)。例如,客户端发送一个 SYN 报文段,ISN 为 1000,这表示客户端想要和服务器建立连接,并且告诉服务器自己初始的序列号是 1000。

第二次握手:服务器收到客户端的 SYN 报文段后,会向客户端发送一个 SYN/ACK 报文段。这个报文段中包含服务器自己的 ISN,同时对客户端的 SYN 进行确认。例如,服务器的 ISN 为 2000,它发送的 SYN/ACK 报文段会确认客户端的序列号(ACK = 1001,表示已经收到客户端的序列号为 1000 的报文段,并且期望下一个收到的是 1001),同时告知客户端自己的序列号为 2000。

第三次握手:客户端收到服务器的 SYN/ACK 报文段后,会向服务器发送一个 ACK 报文段,确认服务器的序列号。例如,客户端发送一个 ACK 报文段,ACK = 2001,表示已经收到服务器的序列号为 2000 的报文段,并且连接正式建立。

四次挥手过程

第一次挥手:主动关闭连接的一方(假设是客户端)发送一个带有 FIN(结束标志)的 TCP 报文段,表示自己没有数据要发送了,想要关闭连接。例如,客户端发送 FIN 报文段,序列号为 3000。

第二次挥手:服务器收到客户端的 FIN 报文段后,会发送一个 ACK 报文段给客户端,确认客户端的 FIN 请求。例如,服务器发送 ACK 报文段,ACK = 3001,表示已经收到客户端的 FIN 请求,但是服务器可能还有数据要发送给客户端,所以此时连接还不能完全关闭。

第三次挥手:当服务器也没有数据要发送给客户端时,服务器会发送一个 FIN 报文段给客户端,请求关闭连接。例如,服务器发送 FIN 报文段,序列号为 4000。

第四次挥手:客户端收到服务器的 FIN 报文段后,会发送一个 ACK 报文段给服务器,确认服务器的 FIN 请求,连接正式关闭。例如,客户端发送 ACK 报文段,ACK = 4001。

为什么是三次握手和四次挥手

三次握手的原因主要是为了建立可靠的连接,防止已失效的连接请求报文段突然又传送到了服务器,从而产生错误。在第一次握手时,客户端发送 SYN 请求,服务器收到后进行第二次握手确认,此时服务器可以确定客户端的发送功能是正常的。客户端收到服务器的第二次握手后进行第三次握手确认,此时客户端可以确定自己的发送功能和服务器的接收、发送功能都是正常的,服务器也可以确定客户端的接收功能是正常的,这样就建立了一个可靠的双向通信连接。

四次挥手的原因是因为 TCP 是全双工通信,客户端和服务器都可以同时进行数据发送和接收。当客户端想要关闭连接时,它发送 FIN 表示自己不再发送数据,但服务器可能还有数据要发送给客户端,所以服务器先确认客户端的 FIN,然后等自己数据发送完后再发送 FIN 给客户端,客户端最后确认服务器的 FIN,这样才能保证双方的数据都能完整地发送和接收,从而实现可靠的连接关闭。

HTTP 和 HTTPS 及其区别、HTTP 请求格式和 HTTPS 的流程、原理、握手过程

HTTP(超文本传输协议)和 HTTPS(超文本传输安全协议)区别

首先,从安全性方面来看,HTTP 是明文传输协议,数据在传输过程中是以明文形式发送的,这就意味着如果数据被截获,攻击者可以很容易地读取其中的内容。例如,在一个 HTTP 的网页登录过程中,用户名和密码都是以明文形式在网络上传输的,很容易被窃取。而 HTTPS 是在 HTTP 的基础上加入了 SSL/TLS(安全套接层 / 传输层安全协议)加密层,通过加密来保证数据的安全性。数据在传输之前会被加密,即使被截获,没有解密密钥也无法读取内容。

其次,从端口号来看,HTTP 默认使用的端口号是 80,而 HTTPS 默认使用的端口号是 443。

再者,从认证方面来看,HTTP 没有身份认证机制,无法验证通信双方的身份。而 HTTPS 可以通过数字证书来验证服务器的身份,并且在一些双向认证的情况下,还可以验证客户端的身份。

HTTP 请求格式

  • 首行:包括请求方法、请求的 URL 和协议版本。例如,“GET /index.html HTTP/1.1”,这里的 “GET” 是请求方法,表示获取资源,“/index.html” 是请求的 URL,即要获取的资源路径,“HTTP/1.1” 是协议版本。
  • 协议头:包含各种请求头信息,用于向服务器提供更多关于请求的细节。例如,“User - Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36”,这个请求头告诉服务器客户端使用的浏览器类型和版本等信息。另外还有 “Accept - Language: zh - CN” 表示客户端期望接收的语言是中文等多种信息。
  • 空行:用于分隔协议头和正文部分,它是必须的,标志着协议头部分的结束。
  • 正文:并不是所有的 HTTP 请求都有正文,一般用于 POST 等请求方法,用于向服务器发送数据。例如,在一个表单提交的 POST 请求中,正文部分可能包含用户在表单中输入的用户名、密码等数据。

HTTPS 的流程、原理和握手过程

流程和原理:HTTPS 的主要目的是在 HTTP 和 TCP 之间建立一个安全的加密通道。当客户端向服务器发起 HTTPS 请求时,首先会建立 TCP 连接(这和 HTTP 一样,通过三次握手建立连接)。然后,客户端和服务器会进行 SSL/TLS 握手过程,在这个过程中,双方会协商加密算法、交换密钥等,用于后续数据的加密和解密。一旦握手成功,客户端和服务器就可以通过加密后的通道进行 HTTP 数据的传输,数据在传输过程中会被加密,到达对方后再进行解密。

握手过程:

  1. 客户端发起请求:客户端向服务器发送一个 ClientHello 消息,这个消息中包含客户端支持的 SSL/TLS 版本、加密算法列表、随机数等信息。例如,客户端告诉服务器自己支持 TLS 1.2 和 TLS 1.3,并且列出了一系列如 RSA、AES 等加密算法,同时发送一个随机数 ClientRandom。
  2. 服务器响应:服务器收到客户端的 ClientHello 消息后,会返回一个 ServerHello 消息,其中包含服务器选择的 SSL/TLS 版本、加密算法、随机数等信息。例如,服务器选择 TLS 1.3,选择了 AES 加密算法,同时发送一个随机数 ServerRandom。然后服务器会发送自己的数字证书给客户端,证书中包含服务器的公钥等信息,用于客户端验证服务器的身份。
  3. 客户端验证:客户端收到服务器的数字证书后,会验证证书的有效性。例如,检查证书是否由权威机构颁发、证书是否过期等。如果证书验证通过,客户端会使用证书中的公钥来加密一个随机的对称密钥(Pre - master - secret),并发送给服务器。
  4. 密钥交换和完成:服务器收到客户端发送的加密后的对称密钥后,使用自己的私钥进行解密,这样双方就都拥有了对称密钥。然后双方会使用这个对称密钥以及之前交换的随机数等信息来生成会话密钥,用于后续的数据加密和解密。最后,双方会发送一个 Finished 消息,表示握手过程完成,之后就可以通过加密后的通道进行 HTTP 数据的传输。

HTTP 里面有哪些常用的方法?GET 与 POST 的区别以及 Get 和 Post 有什么区别?

在 HTTP 中,有多种常用的方法。

GET 方法主要用于从服务器获取资源。比如当用户在浏览器地址栏输入一个网址或者点击网页上的链接时,浏览器通常会发送 GET 请求来获取网页的内容。它的特点是请求的数据会附加在 URL 后面,以 “?” 分隔 URL 和参数,参数之间用 “&” 连接。例如,“https://example.com/search?q=keyword&page=2”,这里 “q=keyword&page=2” 就是传递给服务器的参数,这种方式使得请求的数据对用户是可见的。而且由于 URL 长度的限制,GET 请求传递的数据量相对较小。另外,GET 请求是幂等的,这意味着多次执行相同的 GET 请求,对服务器资源的影响是相同的,不会因为多次请求而改变服务器资源的状态(除了可能会记录访问次数等情况)。

POST 方法主要用于向服务器提交数据,比如在网页上提交表单时,像用户注册、登录、发表评论等操作。POST 请求的数据是放在请求体中的,不会像 GET 请求那样直接显示在 URL 中,所以相对更安全一些,适合传递一些敏感信息,如密码等。而且 POST 请求没有像 GET 请求那样严格的数据量限制,它可以传输大量的数据。POST 请求不是幂等的,多次提交相同的 POST 请求可能会导致服务器端的数据发生多次改变,例如多次提交用户注册表单可能会创建多个相同的用户账号(当然实际应用中会有防止重复提交的机制)。

GET 和 POST 的区别主要体现在以下几个方面。从数据传输位置看,GET 的数据在 URL 中,POST 的数据在请求体中。从安全性来讲,POST 相对更安全,因为数据不在 URL 中暴露。从数据量角度,GET 有 URL 长度限制,POST 没有严格限制。从幂等性方面,GET 是幂等的,POST 不是。

DNS 的解析过程以及服务器 IP 地址改变后客户端如何知晓?

DNS(域名系统)解析过程主要包括以下步骤。

首先是本地解析。当客户端需要访问一个域名时,它会先检查自己的本地缓存。这个缓存可能是之前解析该域名时存储的结果,或者是操作系统预加载的一些常见域名的解析结果。例如,当用户频繁访问某个网站时,第一次解析后的结果会被存储在本地缓存中,下次访问时如果缓存未过期,就可以直接使用缓存中的 IP 地址来访问,这样可以加快访问速度。

如果本地缓存中没有对应的域名解析结果,客户端会向本地 DNS 服务器发送请求。本地 DNS 服务器通常是由互联网服务提供商(ISP)提供的。这个请求包含需要解析的域名。本地 DNS 服务器收到请求后,会先检查自己的缓存。如果缓存中有该域名的解析结果,就直接返回给客户端。

如果本地 DNS 服务器的缓存中也没有,它会向根 DNS 服务器发送请求。根 DNS 服务器知道顶级域名(TLD)服务器的地址。它会根据请求域名中的顶级域名部分(如.com、.org 等),将请求转发给相应的顶级域名服务器。

顶级域名服务器收到请求后,会根据域名中的二级域名等信息,将请求转发给负责该域名的权威 DNS 服务器。权威 DNS 服务器存储了该域名对应的 IP 地址,它会将 IP 地址返回给本地 DNS 服务器,本地 DNS 服务器再将这个 IP 地址返回给客户端。

当服务器 IP 地址改变时,客户端知晓的方式主要有以下几种。一是通过 DNS 缓存的过期机制。DNS 记录是有生存时间(TTL)的,当 TTL 过期后,本地 DNS 服务器会重新进行域名解析,这样就可以获取到更新后的 IP 地址。二是一些动态 DNS 服务。对于一些经常改变 IP 地址的服务器,如家庭网络中的服务器,可能会使用动态 DNS 服务。这些服务会在 IP 地址改变时,主动向 DNS 服务器更新记录,使得客户端下次解析域名时能够获取到新的 IP 地址。另外,在一些企业级网络环境中,可能会通过配置管理工具来手动或者自动更新 DNS 记录,并且通知客户端更新缓存或者重新进行域名解析。

一个 URL 输入到浏览器到显示网页的过程?

当在浏览器中输入一个 URL 后,首先浏览器会进行 URL 解析。它会将 URL 分解为协议、域名、路径、查询参数等部分。例如,对于 URL“Example Domain”,浏览器会识别出协议是 “https”,域名是 “example.com”,路径是 “/index.html”,查询参数是 “param=value”。

接着,浏览器会根据协议来确定如何获取资源。如果是 HTTP 或 HTTPS 协议,就会进行域名解析,这个过程如前面所述,通过 DNS 系统将域名转换为 IP 地址。

在获取到服务器的 IP 地址后,浏览器会与服务器建立 TCP 连接。这是通过三次握手来完成的,客户端(浏览器)向服务器发送 SYN 包,服务器收到后返回 SYN/ACK 包,客户端再发送 ACK 包,这样就建立了一个可靠的连接通道。

建立连接后,浏览器会根据 URL 中的请求方法(如 GET 或 POST 等)向服务器发送 HTTP 请求。请求包括请求行(包含请求方法、URL 和协议版本)、请求头(包含各种信息,如浏览器类型、可接受的内容类型等)和请求体(如果是 POST 等需要提交数据的请求)。

服务器收到请求后,会根据请求的内容进行处理。如果是请求网页资源,服务器会查找对应的网页文件,可能会涉及到服务器端的脚本处理(如 PHP、Python 等后端语言),生成相应的 HTML、CSS、JavaScript 等内容。

服务器处理完请求后,会将响应发送回浏览器。响应包括响应行(包含协议版本、状态码和状态消息)、响应头(包含内容类型、内容长度等信息)和响应体(实际的网页内容)。

浏览器收到响应后,会根据响应头中的内容类型来解析响应体。如果是 HTML 内容,浏览器会开始解析 HTML 文档,构建 DOM(文档对象模型)树。在解析 HTML 的过程中,遇到外部资源引用(如 CSS 文件、JavaScript 文件),浏览器会再次发送请求获取这些资源,并根据它们来渲染网页。对于 CSS 文件,浏览器会根据样式规则来渲染页面的样式;对于 JavaScript 文件,浏览器会执行其中的代码,可能会对 DOM 树进行操作,实现动态的网页效果。最后,浏览器将渲染好的网页显示给用户。

TLS 握手相关知识

TLS(传输层安全协议)握手是在客户端和服务器之间建立安全通信通道的关键过程。

首先是客户端发起握手。客户端向服务器发送一个 “ClientHello” 消息。这个消息包含了客户端支持的 TLS 版本(如 TLS 1.2、TLS 1.3 等)、客户端生成的随机数(称为 ClientRandom)、客户端支持的加密套件列表。加密套件包括加密算法(如 AES、RSA 等)、密钥交换算法、消息认证码算法等组合。例如,一个加密套件可能是 “TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384”,其中 “ECDHE” 是密钥交换算法,“RSA” 是用于身份验证的算法,“AES_256_GCM” 是加密算法,“SHA384” 是消息认证码算法。

服务器收到客户端的 “ClientHello” 消息后,会发送 “ServerHello” 消息作为回应。这个消息包含服务器选择的 TLS 版本(通常是从客户端支持的版本中选择)、服务器生成的随机数(ServerRandom)和服务器选择的加密套件。然后,服务器会发送自己的数字证书给客户端。数字证书包含服务器的公钥、证书颁发机构(CA)的信息等。证书用于向客户端证明服务器的身份是合法的,证书颁发机构是被信任的第三方机构,它对服务器的身份进行了验证。

客户端收到服务器的证书后,会进行验证。客户端会检查证书是否是由信任的 CA 颁发的,证书是否过期,证书的域名是否与服务器的域名匹配等。如果证书验证通过,客户端会使用证书中的公钥来加密一个预主密钥(Pre - master - secret),并发送给服务器。这个预主密钥是通过密钥交换算法生成的,它是后续生成会话密钥的重要部分。

服务器收到客户端发送的加密后的预主密钥后,会使用自己的私钥进行解密。然后,客户端和服务器会使用 ClientRandom、ServerRandom 和预主密钥来生成会话密钥。会话密钥用于后续的数据加密和解密。

最后,客户端和服务器会发送 “Finished” 消息,表示 TLS 握手过程完成。之后,双方就可以使用会话密钥通过加密后的通道进行数据传输,如在 HTTPS 通信中传输 HTTP 数据,保证数据的安全性和完整性。

聊聊数据结构吧,二叉树有什么好处?有什么具体的应用场景?

二叉树是一种重要的数据结构,它有许多好处。

首先,二叉树的结构相对简单且规则。每个节点最多有两个子节点,这种结构使得数据的组织和管理变得有序。与线性结构(如数组、链表)相比,二叉树在插入、删除和查找操作上具有更好的性能。例如,在一个有序数组中插入一个新元素,可能需要移动大量的元素来保持顺序,而在二叉树中,通过比较节点的值,可以快速地找到合适的位置插入新节点,时间复杂度在平均情况下可以达到 O (log n),其中 n 是节点的数量。

其次,二叉树具有很好的层次性。这种层次性可以用于表示具有层次关系的数据。例如,在文件系统中,目录和文件可以用二叉树来表示。目录可以看作是父节点,文件和子目录可以看作是子节点,通过二叉树的结构可以很方便地遍历文件系统,查找特定的文件或目录。

二叉树的具体应用场景有很多。在搜索引擎中,二叉树可以用于构建索引。例如,对于网页中的关键词,可以构建一个二叉搜索树。每个节点存储一个关键词,通过比较关键词的大小,可以快速地在树中查找某个关键词是否存在,以及找到与之相关的网页信息。这种结构可以大大提高搜索的效率。

在编译器的语法分析中,二叉树也有应用。编译器会将程序代码的语法结构解析为一棵语法树,语法树的结构通常是二叉树或者多叉树的形式。通过对语法树的遍历和分析,编译器可以检查程序代码的语法是否正确,并且生成相应的目标代码。例如,对于一个表达式 “3 + 4 * 2”,可以构建一棵语法树,其中 “+” 和 “*” 是节点,数字是叶子节点,通过对这棵语法树的分析,可以按照正确的运算顺序(先乘除后加减)生成目标代码。

在数据压缩算法中,二叉树也发挥作用。例如,哈夫曼树是一种特殊的二叉树,它用于哈夫曼编码。在哈夫曼编码中,出现频率高的字符会被赋予较短的编码,出现频率低的字符会被赋予较长的编码。通过构建哈夫曼树,将字符作为叶子节点,根据字符的频率来构建树的结构,然后可以生成高效的编码方案,从而实现数据的压缩。

HashMap 底层是怎么实现的?有哪些遍历方法?是否线程安全?哪些是线程安全的?讲一下 rehash。

HashMap 底层是基于数组和链表(在 Java 8 之后还引入了红黑树)实现的。它的内部有一个数组,这个数组的每个元素可以看作是一个桶(bucket)。当我们向 HashMap 中放入一个键值对时,首先会根据键的哈希值(通过 hashCode 方法计算)来确定它应该放在数组的哪个位置,也就是哪个桶中。如果计算出来的位置没有冲突,也就是该桶为空,那么就直接将键值对放入这个桶中。如果发生冲突,即多个键计算出来的位置相同,在 Java 7 及以前是采用链表的方式来解决冲突,新的键值对会被添加到该桶对应的链表的头部。在 Java 8 之后,如果链表的长度达到一定阈值(默认为 8),并且数组的长度达到一定条件,这个链表就会被转换为红黑树,这样可以提高查找效率,因为红黑树的查找时间复杂度是 O (logn),而链表是 O (n)。

HashMap 的遍历方法有多种。可以通过 entrySet 来遍历,获取所有的键值对集合,然后通过迭代器或者增强 for 循环来遍历这个集合,在遍历过程中可以同时获取键和值。也可以通过 keySet 获取键的集合,然后通过键来获取值进行遍历,不过这种方式相对效率稍低一些。还可以通过 values 获取值的集合进行遍历,但是这样就无法获取对应的键了。

HashMap 不是线程安全的。在多线程环境下,可能会出现数据不一致的情况。例如,当多个线程同时对 HashMap 进行 put 操作时,可能会导致数据覆盖或者链表形成环等问题。如果要在多线程环境下使用安全的 Map,可以使用 ConcurrentHashMap。

rehash 是在 HashMap 的容量发生变化时进行的操作。当 HashMap 中的元素数量达到一定比例(负载因子)时,为了保持性能,会对数组进行扩容。在扩容之后,原来存储在数组中的键值对的位置可能会发生改变,因为新的数组长度会导致计算出来的哈希位置不同。rehash 的过程就是重新计算每个键值对在新数组中的位置,然后将它们迁移到新的位置上。这个过程相对比较复杂,需要考虑到之前存储的是链表或者红黑树的情况,对于链表要重新计算每个节点的位置,对于红黑树也要进行相应的调整。

ConcurrentHashMap 的原理?

ConcurrentHashMap 是一个线程安全的哈希表,它在多线程环境下能够高效地进行读写操作。

在 Java 7 及以前,ConcurrentHashMap 采用了分段锁的机制。它内部将整个哈希表分成了多个段(Segment),每个段类似于一个独立的小哈希表,并且每个段都有自己独立的锁。当多个线程对 ConcurrentHashMap 进行操作时,如果不同的线程操作的是不同段的数据,那么它们可以并发地进行操作,因为它们获取的是不同的锁。只有当多个线程操作的是同一段的数据时,才需要进行等待,获取该段的锁之后才能进行操作。这种分段锁的设计大大提高了在多线程环境下的并发性能,相比对整个哈希表加锁的方式,能够让更多的线程同时进行读写操作。

在 Java 8 之后,ConcurrentHashMap 的实现进行了优化。它不再采用分段锁的方式,而是采用了 CAS(比较并交换)操作和 synchronized 关键字相结合的方式。在进行 put 操作时,首先会根据键的哈希值找到对应的桶,如果桶为空,会使用 CAS 操作尝试将键值对放入桶中。如果桶不为空,会对桶进行加锁(通过 synchronized 关键字),然后再进行插入操作。这种方式在保证线程安全的同时,进一步提高了并发性能。在读取操作时,几乎不会进行加锁操作,通过一些特殊的设计(如使用 volatile 关键字保证可见性),使得读取操作可以在不加锁的情况下进行,这样可以提高读取的效率。而且在扩容过程中,也采用了一些巧妙的设计,使得扩容可以和其他操作同时进行,不会完全阻塞其他操作。

ArrayList 的扩容能说一说吗?

ArrayList 是 Java 中常用的动态数组。它的容量是可以自动增长的。

当我们创建一个 ArrayList 时,可以指定一个初始容量。如果没有指定,默认会有一个初始容量。当向 ArrayList 中添加元素时,它会检查当前元素的数量是否已经达到了容量。如果还没有达到容量,那么直接将元素添加到数组的末尾就可以了。

当元素数量达到容量时,就需要进行扩容。ArrayList 的扩容机制是创建一个新的、更大容量的数组,然后将原来数组中的元素复制到新数组中。新数组的容量通常是原来容量的一定倍数。在 Java 中,默认情况下,新容量是原来容量的 1.5 倍。例如,原来的容量是 10,当需要扩容时,新的容量会是 15。不过,这个倍数也可以通过构造函数或者其他方式进行修改。

在复制元素的过程中,会将原来数组中的所有元素按照顺序复制到新数组中。这个过程相对来说是比较消耗性能的,因为需要进行大量的数组复制操作。为了尽量减少扩容的次数,在使用 ArrayList 时,如果能够预估元素的数量,可以在创建 ArrayList 时就指定一个合适的容量,这样可以避免频繁的扩容操作,提高性能。另外,在进行大量元素添加操作时,也可以考虑使用其他更合适的数据结构,比如 LinkedList,在某些情况下它可能更适合频繁的插入操作。

比较一下 Java 中的 set、list 和 map。

在 Java 中,Set、List 和 Map 是三种不同的集合接口,它们有各自的特点和用途。

List 是一个有序的集合,它允许存储重复的元素。就像一个队列一样,元素按照插入的顺序进行排列。例如,ArrayList 和 LinkedList 都是 List 接口的实现类。ArrayList 是基于数组实现的,它在随机访问元素时效率很高,因为可以通过索引直接访问数组中的元素,时间复杂度是 O (1)。但是在插入和删除元素时,如果操作的位置不是在末尾,可能需要移动大量的元素,效率相对较低。LinkedList 是基于链表实现的,它在插入和删除元素时效率比较高,尤其是在链表的中间位置进行操作时,只需要修改节点之间的引用关系就可以了,但是在随机访问元素时,需要从链表的头部或者尾部开始遍历,时间复杂度是 O (n)。

Set 是一个不允许存储重复元素的集合。它就像是一个没有重复元素的袋子。HashSet 是 Set 接口的一个常见实现类,它是基于 HashMap 实现的。在向 HashSet 中添加元素时,实际上是将元素作为 HashMap 的键,值则是一个固定的虚拟值。因为 HashMap 的键是不允许重复的,所以 HashSet 也不允许重复元素。Set 主要用于对元素进行去重,或者检查一个元素是否已经存在于集合中。

Map 是一种键值对的集合,它用于存储和检索键值关联的数据。例如,HashMap 就是 Map 接口的一个实现类。通过键可以快速地获取对应的键值对中的值。Map 在实际应用中非常广泛,比如用于存储配置信息,键可以是配置项的名称,值可以是配置项的内容;也可以用于缓存数据,键是缓存的标识符,值是缓存的数据。与 List 和 Set 不同的是,Map 主要关注的是键值对之间的关系,而不是单纯的元素集合。

总的来说,List 侧重于有序的元素集合,Set 侧重于去重后的元素集合,Map 侧重于键值对的存储和检索。

多线程了解吗?在单核 CPU 的情况下,有多个任务,那是单线程执行的时间快还是多线程快呢?

我对多线程比较了解。多线程是指在一个程序中同时运行多个线程,每个线程可以独立地执行任务。

在单核 CPU 的情况下,多个任务的执行时间比较复杂。从直观上看,单线程执行多个任务时,任务是顺序执行的,一个任务完成后再执行下一个任务。而多线程可以让多个任务看起来像是同时执行,因为 CPU 会在多个线程之间快速切换。

然而,在单核 CPU 下,多线程并不一定比单线程快。这是因为线程切换是有成本的。当 CPU 从一个线程切换到另一个线程时,需要保存当前线程的执行状态(如寄存器的值、程序计数器等),然后加载下一个线程的执行状态,这个过程需要花费一定的时间。如果任务比较简单,而且线程切换的频率很高,那么线程切换所花费的时间可能会超过多线程并行执行带来的好处,导致多线程执行的总时间比单线程还长。

但是,如果任务中有一些是需要等待外部资源(如 I/O 操作)的,那么多线程就可能会体现出优势。例如,一个任务是读取文件,另一个任务是进行数据计算。在单线程下,读取文件时 CPU 会处于等待状态,直到文件读取完成才能进行计算。而在多线程下,当一个线程进行文件读取时,另一个线程可以进行计算,这样可以充分利用 CPU 的时间,减少总的等待时间。不过,即使在这种情况下,也需要合理地设计多线程的结构,避免过多的线程切换成本。

Android 里面多线程的应用,线程池相关知识。

在 Android 中,多线程有着广泛的应用。

首先,在进行网络请求时会用到多线程。例如,当一个 Android 应用需要从服务器获取数据,如获取用户信息、新闻列表等,由于网络请求可能会花费较长时间,若在主线程(UI 线程)中进行,会导致界面卡顿。所以会开启一个新的线程来进行网络请求,当请求完成后,再通过一些机制(如 Handler)将结果返回到主线程来更新 UI。

其次,在处理复杂的计算任务时也会用到多线程。比如进行图像的复杂处理,像对图片进行滤镜效果的计算。如果在主线程进行,同样会影响用户体验,使用多线程可以将这些计算任务放在后台线程进行,让主线程能够正常响应用户的操作。

对于线程池,它主要用于管理和复用线程。在 Android 开发中,频繁地创建和销毁线程会带来性能开销。线程池通过预先创建一定数量的线程,将任务提交到线程池中,线程池会分配线程来执行这些任务。线程池有多种类型,如 FixedThreadPool,它会创建固定数量的线程,当有任务提交时,这些线程会执行任务,如果任务数量超过线程数量,任务会在队列中等待。CachedThreadPool 会根据任务的数量动态地创建线程,当有新任务并且没有空闲线程时,会创建新的线程来执行任务,当线程空闲一段时间后会被回收。ScheduledThreadPool 可以用来执行定时任务和周期性任务,对于一些需要定时更新数据或者周期性执行的操作非常有用,比如定时检查服务器是否有新消息推送等。

进程和线程的区别,并发和并行分别是什么意思?多线程是并发还是并行?

进程是一个具有独立功能的程序在某个数据集合上的一次运行活动,它是操作系统进行资源分配和调度的基本单位。进程拥有自己独立的内存空间、文件句柄等资源。例如,当打开一个 Android 应用时,操作系统会为这个应用创建一个进程,这个进程有自己独立的运行环境,包括内存区域来存储应用的数据、代码等。

线程是进程中的一个执行单元,它是比进程更小的能够独立运行的基本单位。一个进程可以包含多个线程,这些线程共享进程的资源,如内存空间。比如在一个 Android 应用进程中,可能有一个线程负责 UI 的显示和更新,另一个线程负责后台的数据下载,它们共享应用进程的内存,这样可以方便地进行数据共享和通信。

并发是指多个任务在一段时间内交替执行,在单核 CPU 的情况下,由于 CPU 一次只能执行一个任务,所以通过快速地在多个任务之间切换来营造出多个任务同时执行的假象。例如,有两个线程 A 和 B,CPU 先执行 A 线程的一部分,然后切换到 B 线程执行一部分,再切换回 A 线程,如此反复。

并行是指多个任务在同一时刻真正地同时执行,这需要多核 CPU 的支持。比如有一个四核 CPU,就可以同时执行四个任务。

多线程在单核 CPU 下是并发,在多核 CPU 下可以是并发也可以是并行。当只有一个核时,多个线程通过 CPU 的调度交替执行;当有多个核时,多个线程可以分配到不同的核上同时执行。

如何保证多线程安全?synchronized 和 ReentrantLock 的区别,你认为哪一种比较好,为什么?synchronized 和 Lock 区别(性能,synchronized 具体做了哪些优化),synchronized 的底层原理?类锁与对象锁的区别?

保证多线程安全主要是通过对共享资源进行同步访问来实现。

synchronized 是 Java 中的关键字,它可以用于修饰方法或者代码块。当一个线程访问被 synchronized 修饰的方法或者代码块时,会获取对象的锁,其他线程想要访问必须等待锁的释放。它的底层原理是基于对象头中的锁标记和监视器(Monitor)来实现的。当一个线程进入被 synchronized 修饰的代码块时,会将对象头中的锁标记设置为锁定状态,并且会和监视器关联。监视器是一种同步机制,它维护了一个锁计数器,当一个线程获取锁时,计数器加 1,当线程释放锁时,计数器减 1,当计数器为 0 时,锁被释放。

ReentrantLock 是一个可重入的互斥锁。和 synchronized 类似,它也用于控制多个线程对共享资源的访问。它提供了更灵活的功能,比如可以设置公平锁和非公平锁。公平锁是指按照线程请求锁的顺序来分配锁,非公平锁则不保证这个顺序。

synchronized 和 ReentrantLock 的区别主要体现在几个方面。在性能方面,在 Java 早期版本中,synchronized 的性能相对较差,但在 Java 6 之后,对 synchronized 进行了大量的优化,例如自适应自旋锁、偏向锁等,使得它的性能在很多情况下和 ReentrantLock 相当。在功能上,ReentrantLock 更加灵活,除了可以设置公平锁外,还提供了一些方法,如 tryLock 方法可以尝试获取锁,而不会一直等待。

对于哪一种比较好,需要根据具体的场景来判断。如果只是简单的同步需求,synchronized 关键字使用起来更加简洁,因为它是 Java 语言内置的,编译器会自动处理一些细节。而如果需要更复杂的功能,如公平锁、可中断锁等,ReentrantLock 会是更好的选择。

类锁是指在一个类的所有对象之间共享的锁,当一个线程获取了类锁,那么这个类的所有对象的同步方法或者代码块都被锁定。对象锁是指针对一个具体对象的锁,不同对象的对象锁是相互独立的。例如,有一个类 A,有两个对象 a1 和 a2,当一个线程获取了 a1 的对象锁时,a2 的对象锁可以被其他线程获取,但是如果获取了类 A 的类锁,那么不管是 a1 还是 a2 的同步方法或者代码块都不能被其他线程访问。

线程之间可以共享什么?

线程之间可以共享进程中的许多资源。

首先是内存空间。在一个进程中,所有线程共享进程的堆内存。堆内存主要用于存储对象实例。例如,在一个多线程的 Java 程序中,一个线程创建了一个对象并将其存储在堆中,其他线程可以访问这个对象。这就使得线程之间可以方便地共享数据,比如多个线程共同操作一个数据集合,如 ArrayList 或者 HashMap 等。

其次是文件资源。如果一个线程打开了一个文件进行读写操作,其他线程在适当的权限下也可以对这个文件进行操作。例如,一个线程正在读取文件中的数据,另一个线程可以将新的数据写入这个文件,不过需要注意文件读写的同步问题,避免数据混乱。

还有静态变量。在类中的静态变量是被所有实例共享的,对于线程来说,也是可以共享的。例如,一个类中有一个静态的计数器变量,多个线程可以对这个计数器进行操作,比如一个线程负责增加计数器的值,另一个线程负责读取计数器的值,不过同样需要考虑多线程安全问题。

另外,像数据库连接等资源也可以在多个线程之间共享。例如,多个线程可以通过共享的数据库连接来查询或者更新数据库中的数据,但是需要合理地管理连接的使用,避免出现资源竞争和冲突。

volatile 理解,JMM 中主存和工作内存到底是啥?和 JVM 各个部分怎么个对应关系?

volatile 是 Java 中的一个关键字,它主要用于保证变量的可见性和禁止指令重排序。

从可见性角度来说,当一个变量被声明为 volatile 时,一个线程对这个变量的修改,其他线程能够立即看到。在没有 volatile 关键字的情况下,线程可能会从自己的工作内存中读取变量的值,而不是从主存中获取最新的值。例如,有两个线程 A 和 B,线程 A 修改了一个普通变量的值,线程 B 可能不会立即看到这个修改,因为它可能还在使用自己工作内存中的旧值。但是如果这个变量是 volatile 的,那么线程 B 能够马上获取到线程 A 修改后的最新值。

从指令重排序角度看,编译器和处理器为了提高程序的运行效率,可能会对指令进行重新排序。但是对于 volatile 变量,这种重排序是被禁止的。这是为了保证程序的正确性,因为指令重排序可能会导致一些逻辑错误。

在 Java 内存模型(JMM)中,主存是所有线程共享的内存区域,所有变量都存储在主存中。工作内存是每个线程独有的,它是线程从主存中读取变量副本的地方。线程对变量的操作都是在工作内存中进行的,比如读取、修改等操作。当一个线程修改了工作内存中的变量副本后,会将这个修改同步到主存中,这样其他线程才能看到这个修改。

和 JVM 各个部分的对应关系是比较复杂的。JVM 主要包括堆、栈、方法区等部分。主存的概念和堆、方法区有一定的关联,因为对象实例和类的信息等存储在堆和方法区中,这些可以看作是主存的一部分内容。工作内存和线程栈有一定的关联,因为线程栈中存储了线程的局部变量等信息,这些局部变量的副本可以看作是存储在工作内存中。不过这种对应关系并不是绝对的,JMM 是一种抽象的模型,用于规范和描述 Java 程序中的内存访问行为。

CAS、各种锁相关知识。

CAS(Compare - And - Swap)是一种乐观锁机制。它包含三个操作数:内存位置(V)、旧的预期值(A)和新值(B)。CAS 的操作过程是,首先会去读取内存位置 V 的值,将其与预期值 A 进行比较,如果两者相等,就将内存位置 V 的值更新为 B;如果不相等,则说明有其他线程已经修改了这个值,当前操作就不执行。例如,在多线程环境下对一个共享变量进行自增操作,使用 CAS 可以避免使用传统的锁机制。CAS 在一些高性能的并发场景中应用广泛,像 Java 中的 Atomic 系列类(如 AtomicInteger、AtomicLong 等)就是基于 CAS 实现的。

在锁方面,有多种类型。互斥锁是一种基本的锁,它保证在同一时刻只有一个线程能够访问被保护的资源。当一个线程获取了互斥锁后,其他线程想要访问该资源就必须等待锁的释放。像 Java 中的 synchronized 关键字就可以看作是一种互斥锁。

可重入锁是指一个线程可以多次获取同一个锁而不会造成死锁。例如,一个方法中调用了另一个被同一个锁保护的方法,对于可重入锁,线程可以顺利进入第二个方法。ReentrantLock 就是典型的可重入锁。

公平锁和非公平锁也是重要的概念。公平锁是指多个线程按照请求锁的先后顺序来获取锁,就像排队一样。非公平锁则不保证获取锁的顺序,可能会导致一些线程长时间等待。ReentrantLock 可以通过构造函数来指定是公平锁还是非公平锁。

读写锁主要用于对共享资源读多写少的场景。它将锁分为读锁和写锁,多个线程可以同时获取读锁来读取共享资源,但是当一个线程获取写锁时,其他线程无论是读还是写都必须等待。这种机制可以提高并发读取的效率。Java 中的 ReentrantReadWriteLock 就是读写锁的实现。

自旋锁是一种比较特殊的锁。当一个线程尝试获取自旋锁而没有成功时,它不会立刻进入阻塞状态,而是会在一个循环中不断地尝试获取锁,就像一直在 “自旋” 一样。这种方式在等待时间较短的情况下可以减少线程上下文切换的开销。

线程 a 等 b,b 等 c 的实现。

在多线程环境中,要实现线程 a 等待线程 b,线程 b 等待线程 c,可以通过多种方式。

一种常见的方式是使用 CountDownLatch。CountDownLatch 是一个同步辅助类,它可以让一个或多个线程等待其他线程完成一系列操作。例如,创建三个 CountDownLatch 对象,分别为 latchA、latchB 和 latchC。线程 a 在执行过程中会调用 latchB.await (),这样线程 a 就会等待线程 b 执行完相关操作。而线程 b 在执行完自己的任务后,会调用 latchC.await (),表示等待线程 c,同时会调用 latchB.countDown (),表示自己的任务完成,这个操作会使 latchB 的计数器减 1,当计数器为 0 时,等待 latchB 的线程 a 就会被唤醒。线程 c 在完成任务后,会调用 latchC.countDown (),唤醒等待 latchC 的线程 b。

另一种方式是使用 CyclicBarrier。CyclicBarrier 可以让一组线程到达一个屏障点后互相等待,当所有线程都到达这个屏障点后,再一起继续执行。可以把线程 a、b、c 看作是一组需要同步的线程。首先创建一个 CyclicBarrier 对象,设置参与的线程数量为 3。线程 a 执行到一定阶段后,会调用 CyclicBarrier 的 await 方法,表示到达屏障点等待其他线程。线程 b 和 c 也一样,当三个线程都到达屏障点后,它们就可以一起继续执行后续的任务。

还可以通过使用线程的 join 方法来实现。在主线程或者其他协调线程中,可以先启动线程 c,然后在线程 b 中调用线程 c 的 join 方法,这样线程 b 就会等待线程 c 执行完毕。接着在线程 a 中调用线程 b 的 join 方法,使得线程 a 等待线程 b,从而实现线程 a 等待线程 b,b 等待 c 的效果。

设计模式,结合业务谈谈你熟悉的设计模式,有用过哪些设计模式?

设计模式是软件开发中经过实践验证的、可复用的解决方案。

以单例模式为例,在业务中,比如数据库连接池的管理就经常会用到单例模式。数据库连接的创建和销毁是比较消耗资源的操作,为了避免频繁地创建和销毁连接,通常会创建一个数据库连接池。这个连接池可以使用单例模式来实现,确保在整个应用程序中只有一个连接池实例。这样可以方便地对连接进行统一管理,例如控制连接的数量、分配和回收连接等。

工厂模式也很实用。假设一个电商系统中有多种商品类型,每种商品有不同的创建过程。简单工厂模式可以根据不同的条件(如商品类型的标识)来创建不同的商品对象。例如,当用户下单购买商品时,系统需要创建对应的商品对象来处理订单,简单工厂可以接收商品类型参数,然后返回相应的商品实例。抽象工厂模式则更复杂,它可以用于创建一系列相关的产品对象。在电商系统中,如果有不同的销售渠道(如线上店铺和线下店铺),每个渠道可能需要不同的商品包装、配送方式等相关产品对象,抽象工厂就可以根据不同的销售渠道来创建一组相关的产品对象,包括商品包装对象、配送对象等。

观察者模式在消息推送系统中有很好的应用。在电商平台中,当商品有新的促销活动或者库存变化时,需要通知关注这些商品的用户。可以把用户看作是观察者,商品看作是被观察的对象。当商品状态发生变化时,会通知所有关注它的用户。这样可以实现松耦合的设计,商品对象不需要知道具体有哪些用户在关注它,只需要在状态变化时发送通知,而用户对象可以根据自己的需求来处理接收到的通知。

单例模式,问了下双重校验锁的实现和静态内部类的 volatile。

单例模式是一种创建型设计模式,其目的是确保一个类只有一个实例,并提供一个全局访问点。

双重校验锁是一种在多线程环境下实现单例模式的高效方式。首先,在单例类中有一个私有静态变量来存储单例对象,例如 “private static volatile Singleton instance;”,这里的 volatile 关键字很关键,它保证了变量的可见性和禁止指令重排序。在获取单例对象的方法中,会先进行第一次校验,例如 “if (instance == null)”,这是为了避免不必要的同步操作。如果第一次校验通过,就会进入同步块,同步块通常是使用 synchronized 关键字修饰的代码块,在这里是对单例类的类对象进行同步,如 “synchronized (Singleton.class)”。在同步块中会进行第二次校验 “if (instance == null)”,这是因为可能有多个线程同时通过了第一次校验,进入了同步块,第二次校验可以确保只有一个线程创建单例对象。如果第二次校验通过,就会创建单例对象,如 “instance = new Singleton ();”。这种双重校验的方式在保证线程安全的同时,也提高了性能,因为大部分情况下不需要进入同步块就可以获取单例对象。

对于静态内部类的单例模式,它利用了类加载机制来保证单例。单例类有一个静态内部类,在这个内部类中有一个静态变量来存储单例对象。例如,外部单例类是 “Singleton”,内部类是 “private static class SingletonHolder {private static final Singleton INSTANCE = new Singleton ();}”。当外部类被加载时,内部类不会立即被加载。只有当第一次访问内部类中的单例对象时,内部类才会被加载,并且会创建单例对象。由于类加载过程是线程安全的,所以这种方式不需要额外的同步操作就可以保证单例。这里的 volatile 关键字如果用于内部类中的单例对象,同样是为了保证可见性和禁止指令重排序,确保在多线程环境下单例对象的正确性。

简单工厂、抽象工厂是什么?

简单工厂模式是一种创建型设计模式。它的主要作用是将对象的创建和使用分离。简单工厂有一个工厂类,这个工厂类中有一个创建产品对象的方法。例如,有一个产品接口 “Product”,有多个实现这个接口的产品类 “ProductA”、“ProductB” 等。工厂类 “SimpleFactory” 中有一个方法 “createProduct (String type)”,这个方法根据传入的类型参数(如 “type” 的值为 “A” 或 “B”)来创建相应的产品对象。如果 “type” 为 “A”,就返回一个 “ProductA” 对象;如果为 “B”,就返回一个 “ProductB” 对象。这样,在使用产品对象的代码中,不需要知道具体的产品对象是如何创建的,只需要从工厂类中获取就可以了。简单工厂模式使得代码的维护和扩展更加方便,当需要添加新的产品类型时,只需要在工厂类的创建方法中添加相应的逻辑来创建新的产品对象。

抽象工厂模式是比简单工厂模式更复杂的创建型设计模式。它主要用于创建一系列相关的产品对象。假设一个系统中有多个产品族,每个产品族包含多个产品。例如,有两个产品族 “FamilyA” 和 “FamilyB”,每个产品族都有产品 “Product1” 和 “Product2”。抽象工厂模式中有一个抽象工厂类,它定义了创建一系列产品的抽象方法,如 “createProduct1 ()” 和 “createProduct2 ()”。然后有多个具体的工厂类来实现这个抽象工厂类,比如 “ConcreteFactoryA” 用于创建 “FamilyA” 产品族的产品,“ConcreteFactoryB” 用于创建 “FamilyB” 产品族的产品。在具体的工厂类中,会实现抽象工厂类中的抽象方法来创建相应的产品对象。抽象工厂模式的优势在于可以方便地切换产品族,当需要使用不同产品族的产品时,只需要切换具体的工厂类就可以了。它适用于创建对象的过程比较复杂,并且产品之间存在关联关系的场景。

责任链模式相关,如 retrofit intercept 操作了解么?让你设计拦截器怎么设计?

责任链模式是一种行为设计模式,它将请求的发送者和接收者解耦,多个接收者可以组成一个链,请求沿着这个链依次传递,直到有一个接收者处理这个请求。

Retrofit 中的拦截器(Interceptor)就运用了责任链模式。Retrofit 拦截器可以用于在网络请求发送前和响应返回后进行一些操作,比如添加公共的请求头、对请求参数进行加密、对响应数据进行解密或者日志记录等。

如果要设计拦截器,可以从以下几个方面考虑。首先,定义一个拦截器接口,这个接口有一个方法用于处理请求和响应。例如,接口中有一个方法 “intercept (Chain chain)”,“Chain” 是一个代表请求 - 响应链的接口,它有方法用于获取请求和发送请求,在 “intercept” 方法中,可以先获取当前的请求,对请求进行处理,比如添加请求头或者修改请求参数等操作。然后通过 “chain.proceed (request)” 来将处理后的请求发送出去,这个方法会返回一个响应。在获取响应后,又可以对响应进行处理,比如检查响应状态码、解析响应数据等。

在构建拦截器链时,可以通过一个列表或者其他数据结构来存储拦截器。当一个请求到来时,从列表的第一个拦截器开始,依次调用每个拦截器的 “intercept” 方法,每个拦截器处理完后将请求传递给下一个拦截器,直到最后一个拦截器处理完请求并返回响应。这样就形成了一个责任链,每个拦截器只关注自己的处理逻辑,而不需要知道整个请求 - 响应的全部过程。并且这种方式很容易添加、删除或者修改拦截器,增强了系统的灵活性和可维护性。

了解过 Java 虚拟机吗 JVM?Java 文件具体是怎么运行的?

我了解 Java 虚拟机(JVM)。JVM 是 Java 程序的运行环境,它负责执行 Java 字节码。

当有一个 Java 文件时,首先要经过编译阶段。Java 编译器(javac)会将 Java 源文件(.java)编译成字节码文件(.class)。字节码是一种中间形式的代码,它具有平台无关性,这也是 Java 能够 “一次编写,到处运行” 的原因之一。

在运行阶段,JVM 会加载字节码文件。加载过程是由类加载器(ClassLoader)完成的。类加载器有多种,包括引导类加载器、扩展类加载器和应用程序类加载器。引导类加载器负责加载 Java 核心类库,如 java.lang 包中的类;扩展类加载器负责加载 Java 的扩展类库;应用程序类加载器负责加载用户编写的类。

加载后的字节码会被存储在 JVM 的方法区(Method Area),方法区用于存储类的信息,包括类的结构、常量池、静态变量等。同时,每个线程会有自己的程序计数器(Program Counter)和栈(Stack)。程序计数器记录了当前线程执行的字节码行号,栈用于存储方法调用的栈帧。当一个方法被调用时,会在栈中创建一个栈帧,栈帧中包含局部变量表、操作数栈等信息。

JVM 执行字节码是通过解释器和即时编译器(JIT)来完成的。解释器会逐行解释执行字节码,这种方式执行速度相对较慢。JIT 编译器会在运行过程中,将一些热点代码(经常被执行的代码)编译成机器码,这样在后续执行这些代码时,就可以直接以机器码的形式快速执行,提高了执行效率。在执行过程中,JVM 还会管理内存,分配和回收对象所占用的内存空间,并且处理多线程相关的事务,确保 Java 程序的正确运行。

JVM 垃圾回收,内存管理,新生代老年代都说一下。

JVM 的垃圾回收(GC)主要是为了自动回收不再使用的对象所占用的内存空间,避免内存泄漏和内存溢出。

在内存管理方面,JVM 将内存主要划分为堆(Heap)、栈(Stack)、方法区(Method Area)等区域。栈主要用于存储线程的局部变量和方法调用信息,方法区用于存储类的结构、常量池、静态变量等信息,而堆是垃圾回收主要关注的区域,因为对象实例都是在堆中分配的。

堆又进一步分为新生代(Young Generation)和老年代(Old Generation)。新生代是新创建的对象存储的地方,它又分为伊甸区(Eden)和两个 Survivor 区(Survivor0 和 Survivor1)。当一个新对象被创建时,它首先会被分配到伊甸区。伊甸区的空间是有限的,当伊甸区满了之后,就会触发一次 Minor GC(新生代垃圾回收)。在 Minor GC 过程中,伊甸区中还存活的对象会被复制到一个 Survivor 区(假设是 Survivor0),并且对象的年龄会加 1。如果 Survivor0 区满了,存活的对象会被复制到 Survivor1 区,并且年龄继续加 1。当一个对象的年龄达到一定阈值(通常是 15,这个阈值可以通过参数调整),它就会被晋升到老年代。

老年代主要存储生命周期较长的对象。当老年代的空间不足时,会触发 Major GC(老年代垃圾回收)或者 Full GC(全堆垃圾回收,包括新生代和老年代)。Full GC 的成本比 Minor GC 高很多,因为它需要扫描整个堆来回收垃圾对象。

JVM 有多种垃圾回收算法。标记 - 清除算法是一种基础的算法,它首先标记出所有需要回收的对象,然后进行清除。但是这种算法会产生内存碎片。复制算法主要用于新生代,它将内存分为两个相等的区域,每次只使用其中一个区域,当进行垃圾回收时,将存活的对象复制到另一个区域,这种算法不会产生内存碎片,但是会浪费一定的内存空间。标记 - 整理算法主要用于老年代,它在标记出需要回收的对象后,将存活的对象向一端移动,然后清理掉边界以外的内存空间,这样可以避免内存碎片。

Java 中 public,protected,default(什么也不写),private 的区别,子类可以继承父类哪些访问限定符修饰的方法(public,protected,default(什么也不写)),如何使得一个函数不被覆写(final)。

在 Java 中,public、protected、default(什么也不写)和 private 是访问控制修饰符,它们用于控制类、成员变量和方法的访问权限。

public 是最宽松的访问权限。被 public 修饰的类、方法或者成员变量可以在任何类中被访问,不管这些类是否在同一个包中或者是否有继承关系。例如,一个 public 的方法可以在不同包的其他类中被直接调用。

protected 修饰的成员可以在同一个包中的类访问,并且在不同包的子类中也可以访问。这使得在继承关系中,子类可以访问父类的 protected 成员。例如,父类有一个 protected 的方法,子类可以在自己的方法中调用这个父类的 protected 方法。

default(什么也不写)的访问权限是包访问权限。这意味着只有在同一个包中的类才能访问这些成员。如果一个类中的方法没有写访问控制修饰符,那么在同一个包中的其他类可以访问这个方法,但是在不同包中的类就不能访问。

private 是最严格的访问权限。被 private 修饰的成员只能在自己所属的类内部访问,即使是子类也不能访问父类的 private 成员。

子类可以继承父类的 public 和 protected 方法。对于 default 方法,如果子类和父类在同一个包中,子类可以继承;如果不在同一个包中,子类不能继承。

如果要使一个函数不被覆写,可以使用 final 关键字修饰这个函数。当一个方法被标记为 final 时,子类不能重写这个方法。这可以用于确保某个方法的行为在继承过程中不会被改变。例如,在一个工具类中有一些核心的计算方法,为了保证这些方法的正确性和稳定性,就可以将它们标记为 final,防止子类在重写过程中引入错误。

静态内部类和匿名内部类的区别,内部类如何调用外部类的方法(Outter.this. 方法名)。

静态内部类是指被声明为 static 的内部类。它和外部类的联系相对较弱。静态内部类不持有外部类的实例引用,这意味着它可以在没有外部类实例的情况下被创建。例如,外部类是 “Outer”,内部类是 “StaticInner”,可以通过 “Outer.StaticInner inner = new Outer.StaticInner ();” 来创建内部类的实例,不需要先创建外部类的实例。静态内部类只能访问外部类的静态成员,因为它没有和外部类的实例相关联。

匿名内部类是一种没有名字的内部类。它通常用于创建只需要使用一次的类。匿名内部类一般是在创建对象的同时定义类的内容。例如,在创建一个接口的实现对象或者抽象类的子类对象时,可以使用匿名内部类。匿名内部类可以访问外部类的成员变量和方法,因为它持有外部类的实例引用。匿名内部类在创建时会隐式地继承一个类或者实现一个接口,它的定义通常是在一个表达式中完成的,比如 “new Interface () { // 实现接口的方法 }” 或者 “new AbstractClass () { // 重写抽象类的方法 }”。

对于内部类调用外部类的方法,在非静态内部类中,可以使用 “Outer.this. 方法名” 的方式来调用外部类的方法。这是因为非静态内部类持有外部类的实例引用。例如,外部类 “Outer” 中有一个方法 “outerMethod ()”,内部类 “Inner” 中想要调用这个方法,可以在内部类的方法中使用 “Outer.this.outerMethod ();”。而在静态内部类中,由于它没有外部类的实例引用,只能访问外部类的静态方法,直接通过外部类名来调用,如 “Outer.staticMethod ();”。

内存泄漏与内存溢出关系,判断对象是否已死(两次标记:可达性分析 + finalize 方法),详细说 java 四种引用(强引用,软引用,弱引用,虚引用)。

内存泄漏是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费。内存溢出则是指程序在申请内存时,没有足够的内存供其使用。内存泄漏可能会导致内存溢出,当泄漏的内存不断积累,最终可能耗尽所有可用内存,引发内存溢出。

判断对象是否已死采用可达性分析和 finalize 方法结合的两次标记过程。可达性分析是从一系列被称为 GC Roots 的对象开始向下搜索,如果一个对象到 GC Roots 没有任何引用链相连,那么这个对象是初步判定为可回收的。之后,在第一次标记并筛选后,会对对象执行 finalize 方法,若这个对象在 finalize 方法中重新建立了与引用链上某个对象的关联,那么它将再次逃脱被回收的命运;若没有,那这个对象就会被真正标记为可回收。

Java 的四种引用各有特点。强引用是最常见的,如 “Object obj = new Object ();”,只要强引用存在,垃圾回收器就不会回收被引用的对象。软引用是用来描述一些还有用但并非必需的对象,在系统将要发生内存溢出异常之前,会把这些对象列入回收范围进行二次回收,如果内存充足,这些软引用对象可以继续存活。弱引用的强度比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾回收发生之前,当垃圾回收器工作时,无论内存是否足够,都会回收弱引用对象。虚引用是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例,它的作用主要是能在对象被回收时收到一个系统通知。

Java 泛型,反射。

Java 泛型是一种参数化类型的机制。它使得代码可以在编译阶段进行类型检查,提高了代码的安全性和可读性。通过泛型,可以创建类型安全的集合。比如,使用 List<String> list = new ArrayList<String>();,这样就确保了这个列表中只能存放字符串类型的元素。泛型还可以用于自定义类和方法。在自定义类时,可以定义类型参数,例如一个简单的泛型类 class GenericClass<T> {private T value;},这里的 T 可以在创建对象时指定具体类型。在方法中也可以使用泛型,比如 public <T> void genericMethod (T param) {},可以接受不同类型的参数。泛型可以避免类型转换异常,减少代码中的错误。

反射是 Java 的一种强大机制,它允许程序在运行时获取类的信息并操作类或对象的属性和方法。通过反射,可以在运行时动态地创建类的实例、调用类的方法、访问和修改类的成员变量。例如,通过 “Class.forName ("ClassName")” 可以获取一个类的 Class 对象,然后通过这个 Class 对象可以创建实例 “newInstance ()”。也可以通过反射获取类的方法 “getMethod ("methodName", parameterTypes)”,并调用 “invoke (object, args)”。反射在很多框架中都有广泛应用,比如在 Spring 框架中用于依赖注入和 AOP 等功能,它使得程序更加灵活,可以根据配置动态地加载和使用不同的类和方法。

类的 equals 重写。

在 Java 中,equals 方法用于比较两个对象是否相等。当需要根据对象的内容而不是内存地址来判断两个对象是否相等时,就需要重写 equals 方法。

首先,equals 方法的签名应该是 “public boolean equals (Object obj)”,这里参数是 Object 类型,这是为了可以接收任何类型的对象进行比较。在重写 equals 方法时,有几个重要的原则。如果比较的两个对象是同一个对象(通过 “==” 判断),那么它们一定相等,应该返回 true。对于 null 值,如果当前对象不为 null,而传入的对象是 null,那么它们不相等,返回 false。

然后,需要根据对象的属性来判断相等性。例如,如果是一个自定义的类 “Person”,有属性 “name” 和 “age”,在重写 equals 方法时,可以这样实现:先判断传入的对象是否是 “Person” 类型,如果不是,返回 false;如果是,再比较两个 “Person” 对象的 “name” 和 “age” 属性是否相等,如果都相等,返回 true,否则返回 false。需要注意的是,如果重写了 equals 方法,通常也需要重写 hashCode 方法,因为在一些集合类(如 HashMap、HashSet)中,是先根据 hashCode 值来判断对象是否可能相等,然后再用 equals 方法进一步确认,如果只重写 equals 方法而不重写 hashCode 方法,可能会导致这些集合类的行为不符合预期。

OOM 遇到过么,怎么确认位置。

内存溢出(OOM)是 Java 开发中可能遇到的问题。常见的 OOM 有几种类型,比如堆内存溢出(OutOfMemoryError: Java heap space)、栈内存溢出(StackOverflowError)等。

当遇到堆内存溢出时,首先可以通过查看日志来获取一些线索。日志中会显示内存溢出的错误信息以及相关的栈追踪。可以分析是哪些操作可能导致大量对象的创建和占用内存。例如,如果是在处理大量数据并将其存储在集合中,可能是数据量超过了堆内存的限制。可以使用一些内存分析工具,如 Eclipse Memory Analyzer(MAT)。通过获取堆转储文件(heap dump),MAT 可以分析堆中的对象,找出占用大量内存的对象,查看这些对象的引用关系,从而确定是哪些代码导致了内存的过度占用。

对于栈内存溢出,通常是由于方法调用层次过深或者递归没有正确的终止条件。可以查看栈追踪信息,找出递归方法或者方法调用链中可能存在的问题。例如,如果有一个递归算法计算斐波那契数列,没有设置合适的终止条件,就可能导致栈内存溢出。可以通过检查递归的边界条件和逻辑来解决。

另外,还有可能是元空间(Metaspace)内存溢出,这可能是由于加载了过多的类或者动态生成了大量的类信息。可以检查类加载的相关代码,查看是否有不必要的类加载或者类信息的过度生成。

技术上的最大突破。

在技术发展历程中,有很多堪称突破的成就。以软件开发领域为例,面向对象编程(Object - Oriented Programming,OOP)是一个重大突破。它改变了程序设计的思维方式,将数据和操作数据的方法封装在一起形成对象。这种方式使得代码的可维护性、可扩展性和可复用性大大提高。例如,在大型软件项目中,可以创建各种类来代表不同的实体,如用户类、订单类等,每个类有自己的属性和方法,不同的类之间通过继承、多态等关系相互关联。通过面向对象编程,可以更清晰地组织代码结构,降低模块之间的耦合度。

另一个突破是互联网技术的发展。网络协议的不断完善,使得全球范围内的信息共享和通信成为可能。特别是 HTTP 协议的广泛应用,构建了万维网的基础。通过 HTTP 协议,浏览器可以向服务器请求网页资源,服务器能够准确地响应请求,实现了网页的浏览、信息的查询等功能。这使得人们可以方便地获取各种知识和服务,促进了电子商务、在线教育、社交媒体等众多行业的蓬勃发展。

还有数据库技术的革新。关系型数据库的出现,如 MySQL、Oracle 等,为数据的存储和管理提供了高效、可靠的解决方案。它通过表格的形式存储数据,利用 SQL 语言进行数据的查询、插入、修改和删除操作。这种结构化的数据存储方式使得数据的管理更加规范化,能够处理大量的数据,并保证数据的一致性和完整性。同时,非关系型数据库(NoSQL)的发展也为处理海量、非结构化数据提供了新的途径,满足了不同应用场景下的数据存储需求。这些技术突破都深刻地影响了整个信息技术领域的发展。

内部类会有内存泄漏问题吗?内部类为什么能访问外部类的变量,为什么还能访问外部类的私有变量。

内部类是有可能出现内存泄漏问题的。在非静态内部类中,如果内部类的生命周期长于外部类,并且内部类持有外部类的引用,就可能导致外部类无法被垃圾回收,从而产生内存泄漏。例如,在一个 Activity(安卓中的界面组件)中定义了一个内部类用于异步任务(如网络请求),如果这个内部类在异步任务完成后还持有 Activity 的引用,而此时 Activity 应该被销毁,由于引用关系,Activity 无法被垃圾回收,就出现了内存泄漏。

内部类能够访问外部类的变量是因为内部类持有外部类的一个隐式引用。在编译时,编译器会对内部类进行处理,实际上内部类会有一个指向外部类实例的引用。当内部类访问外部类的变量时,是通过这个引用去获取外部类的变量。

对于访问外部类的私有变量,这是 Java 语言的一种设计机制。虽然变量是私有,但是在同一个类的内部,包括内部类,是被允许访问的。这是因为内部类从某种程度上可以看作是外部类的一个成员,和外部类的其他成员(包括私有成员)处于相同的作用域级别。这种访问权限的设置在很多场景下是很有用的,比如在设计模式中的一些实现,像观察者模式,内部类(观察者)可能需要访问外部类(被观察对象)的私有状态来进行相应的处理。

手机要下载视频,你该怎么设计,需要考虑哪些因素?下载后的回调函数该放在子线程还是主线程中?

如果要设计手机视频下载功能,首先要考虑的是下载源。需要确定视频的来源,是从服务器的特定接口获取,还是从其他存储设备(如本地网络中的共享文件夹)获取。对于从服务器下载,要考虑服务器的协议支持,例如是否支持 HTTP、HTTPS 或者其他自定义协议。

在下载过程中,要考虑网络连接的稳定性。需要设置合适的网络超时机制,当网络连接出现问题或者下载速度过慢时,能够及时做出响应,比如暂停下载、重新尝试等。同时,要考虑流量的使用情况,特别是在移动数据网络下,应该提供给用户流量使用的提示和控制选项,例如可以让用户设置仅在 Wi - Fi 环境下下载或者限制下载流量。

对于下载的进度,应该有一个可视化的展示,如进度条,让用户清楚地知道下载的进度。并且要考虑视频文件的大小和手机存储的容量,在下载之前要检查手机剩余存储空间是否足够。

关于下载后的回调函数,理想情况下应该先在子线程中处理。因为视频下载完成后的一些操作,如文件的存储、格式的验证等可能会比较耗时。如果放在主线程,可能会导致手机界面卡顿。但是如果回调函数涉及到更新 UI,如显示下载完成的提示、自动播放视频等操作,就需要将这些操作切换到主线程来完成,因为在安卓和 iOS 等操作系统中,UI 更新操作必须在主线程进行。

接口和抽象的理解。

接口是一种完全抽象的类型,它只包含方法签名而没有方法体。接口的主要目的是定义一组行为规范,规定实现这个接口的类必须要实现这些方法。例如,在一个图形绘制系统中,可能有一个 “Drawable” 接口,它定义了 “draw ()” 方法,任何实现了 “Drawable” 接口的类,如 “Circle”、“Square” 等图形类,都必须实现 “draw ()” 方法来定义自己的绘制方式。接口可以实现多继承,一个类可以实现多个接口,这使得类可以具备多种不同的行为规范。

抽象类是一种不能被实例化的类,它可以包含抽象方法和非抽象方法。抽象方法同样没有方法体,和接口类似,要求子类必须实现这些抽象方法。但是抽象类还可以有非抽象方法,这些方法可以提供一些通用的功能或者逻辑。例如,在一个动物抽象类 “Animal” 中,有抽象方法 “move ()”,不同的动物子类(如 “Bird”、“Fish”)需要实现 “move ()” 方法来定义自己的移动方式,同时 “Animal” 类可以有非抽象方法 “eat ()”,这个方法可能有一个通用的实现,比如 “动物进食” 的基本逻辑。抽象类体现了一种继承关系中的通用和特殊的层次结构,它用于提取子类的共同特征和行为,并且规定子类必须实现的抽象部分。

接口和抽象类的区别在于接口更加纯粹是一种行为规范的定义,没有具体的实现部分,而抽象类可以有部分实现。接口侧重于定义不同类之间的一种契约,抽象类侧重于在继承关系中提取通用的部分和规定抽象部分。

如何理解依赖反转?如何理解多态?

依赖反转是一种软件设计原则,它强调高层模块不应该依赖低层模块,而是两者都应该依赖于抽象。例如,在一个软件系统中有一个用户界面模块(高层模块)和一个数据库访问模块(低层模块)。按照传统的设计,用户界面模块可能直接调用数据库访问模块的方法来获取数据,这样就导致用户界面模块依赖于数据库访问模块的具体实现。在依赖反转原则下,应该引入一个抽象层,比如一个数据访问接口,用户界面模块依赖于这个接口,而数据库访问模块实现这个接口。这样,当数据库的实现方式(如从关系型数据库切换到非关系型数据库)发生变化时,只要新的数据库访问模块实现了这个接口,用户界面模块就不需要做修改。这种方式降低了模块之间的耦合度,提高了系统的可维护性和可扩展性。

多态是面向对象编程中的一个重要概念。它指的是同一个行为具有多种不同表现形式。例如,在一个图形绘制程序中,有一个 “draw” 方法,当这个方法作用于圆形对象时,会绘制出一个圆形;当作用于方形对象时,会绘制出一个方形。这是因为圆形类和方形类都实现了一个 “Drawable” 接口或者继承自一个抽象的 “Shape” 类,并且重写了 “draw” 方法。在程序运行时,根据对象的实际类型来决定调用哪个具体的 “draw” 方法。多态可以通过继承和接口实现。通过继承,子类可以重写父类的方法;通过接口,实现接口的类可以实现接口中定义的方法。多态使得代码更加灵活,可以根据不同的对象类型执行不同的操作,而不需要为每种类型编写不同的代码,提高了代码的复用性。

进程间通信的方式,操作系统进程间通信方式?这些方式间的区别?

在操作系统中,进程间通信(IPC)有多种方式。

管道(Pipe)是一种简单的进程间通信方式,它通常用于具有父子关系的进程之间。管道是一种半双工通信方式,数据只能单向流动。例如,一个父进程可以通过管道向子进程发送数据,或者子进程向父进程发送数据,但不能同时双向通信。管道有匿名管道和命名管道之分。匿名管道只能用于具有亲缘关系的进程之间,因为它没有名字,无法被其他无关进程访问;命名管道有名字,可以被不同的进程通过名字来识别和访问,所以它可以用于无亲缘关系的进程之间。

消息队列(Message Queue)是一种基于消息的通信方式。进程可以向消息队列发送消息,也可以从消息队列接收消息。消息队列是一个由操作系统维护的队列结构,它可以存储多个消息,消息的发送和接收是异步的。不同的进程可以通过消息队列的标识符来访问它,这种方式可以用于多个进程之间的通信,而且消息的发送和接收顺序可以通过队列的特性来控制。

共享内存(Shared Memory)是一种高效的进程间通信方式。它允许不同的进程访问同一块物理内存区域。通过这种方式,进程之间可以快速地交换数据,因为不需要进行数据的复制等操作。但是共享内存也有一些问题,比如需要解决进程之间的同步和互斥问题,否则可能会导致数据的不一致。通常需要配合信号量等机制来保证数据的正确访问。

信号量(Semaphore)主要用于进程之间的同步和互斥。它是一个计数器,用于控制对共享资源的访问。例如,当一个资源有限(如打印机),可以使用信号量来控制多个进程对打印机的访问。信号量的值表示可用资源的数量,当一个进程想要访问资源时,会先检查信号量的值,如果大于 0,就可以访问,并且信号量的值减 1;当进程释放资源时,信号量的值加 1。

套接字(Socket)主要用于不同主机上的进程间通信,也可以用于同一主机上的进程间通信。它基于网络协议(如 TCP/IP),通过 IP 地址和端口号来识别进程。套接字通信可以实现可靠的、双向的通信,并且可以跨越网络进行通信,应用非常广泛,如网络应用程序之间的通信等。

这些方式的区别主要体现在通信的效率、复杂性、可靠性等方面。管道通信简单但功能有限,适用于简单的父子进程间通信;消息队列提供了更灵活的消息传递方式,适用于需要异步通信的场景;共享内存效率高但需要处理同步问题;信号量主要用于同步和互斥,不是直接的通信方式;套接字功能强大,可以实现跨主机通信,但相对复杂,开销较大。

同步与异步的区别?

同步和异步是两种不同的执行模式。

在同步操作中,程序按照顺序依次执行每个任务,一个任务执行完后才会开始执行下一个任务。比如在一个简单的文件读取操作中,如果是同步方式,程序会发起读取请求,然后等待文件系统将文件内容读取完成后才继续执行后续代码。这意味着如果读取文件耗时较长,整个程序会在这期间处于阻塞状态,不能执行其他任务。同步操作的优点是逻辑简单,易于理解和调试,因为执行顺序是明确的。它适用于一些简单的、对执行顺序要求严格且任务执行时间较短的场景。

异步操作则不同,当发起一个异步任务后,程序不会等待这个任务完成,而是继续执行后面的代码。还是以文件读取为例,在异步读取时,程序发起读取请求后就立即执行下一行代码,当文件读取完成后,会通过回调函数或者其他机制来通知程序进行后续处理。这样在等待文件读取的过程中,程序可以去做其他事情,比如处理用户界面的交互等。异步操作的优点是可以提高程序的执行效率和资源利用率,特别是在处理一些耗时操作(如网络请求、I/O 操作等)时,可以避免阻塞整个程序。不过,异步操作的缺点是逻辑相对复杂,因为涉及到任务的回调、状态管理等,容易出现一些难以调试的问题,比如回调地狱等情况。

线程的中断,try/catch 怎么处理异常啥的,try catch finally 返回值问题。

线程中断是一种用于停止线程执行的机制。当一个线程需要被终止时,可以通过设置中断标志来实现。一个线程可以通过调用interrupt方法来设置自身或者其他线程的中断标志。被中断的线程需要能够响应这个中断,一般是通过在合适的位置检查中断标志来决定是否停止执行。例如,在一个循环执行任务的线程中,可以在每次循环开始时检查Thread.currentThread().isInterrupted(),如果返回true,则停止执行任务。

try/catch是 Java 中用于处理异常的重要机制。当可能出现异常的代码被放在try块中时,如果在执行try块内的代码时发生了异常,程序会立即停止try块内后续代码的执行,并开始在catch块中查找与抛出异常类型匹配的处理程序。catch块可以有多个,每个catch块针对不同类型的异常进行处理。例如,如果有一个读取文件的操作在try块中,当文件不存在时会抛出FileNotFoundException,当没有读取权限时会抛出SecurityException,可以分别使用不同的catch块来处理这两种异常,在catch块中可以进行一些错误恢复操作,比如提示用户、记录日志等。

关于try/catch/finally的返回值问题比较复杂。如果try块中有return语句,在执行这个return语句之前,会先执行finally块中的代码。如果finally块中也有return语句,那么finally块中的return会覆盖try块中的return值。如果finally块中没有return语句,但是对try块中要返回的变量进行了修改,那么修改后的值会作为返回值。例如,try块中有int a = 10; return a;,在finally块中修改了a的值为20,那么最终返回的值就是20。如果在try或catch块中抛出了新的异常,并且没有在catch块中处理,那么这个新异常会覆盖原来的异常,并且在finally块执行完后继续向上抛出。

非对称加密相关(结合 HTTPS 讲)

非对称加密是一种重要的加密技术,在 HTTPS 中有着关键的应用。

在非对称加密中,有一对密钥,分别是公钥和私钥。公钥可以公开给任何人,私钥则由持有者妥善保管。用公钥加密的数据只能用私钥解密,反之,用私钥加密的数据只能用公钥解密。在 HTTPS 通信中,服务器会持有私钥和公钥对。当客户端发起 HTTPS 请求时,服务器会将自己的公钥发送给客户端(这个公钥通常是包含在服务器的数字证书中)。

客户端收到公钥后,会使用这个公钥对一个随机生成的对称密钥(也称为会话密钥)进行加密,然后将加密后的对称密钥发送给服务器。因为只有服务器拥有对应的私钥,所以只有服务器能够解密这个对称密钥。这样就完成了客户端和服务器之间对称密钥的安全交换。

之后,客户端和服务器之间的数据传输就使用这个对称密钥进行加密和解密。使用对称密钥加密数据的速度比非对称加密要快很多,所以这种先通过非对称加密交换对称密钥,再用对称密钥加密后续数据的方式,既保证了密钥交换的安全性,又保证了数据传输的效率。非对称加密还可以用于数字签名,服务器可以用自己的私钥对数据进行签名,客户端收到数据后可以用服务器的公钥来验证签名,以确保数据的来源和完整性。这种方式在保证 HTTPS 通信的安全性方面起着至关重要的作用,防止数据在传输过程中被窃取、篡改等。

最近更新:: 2025/10/22 15:36
Contributors: luokaiwen