rokevin
移动
前端
语言
  • 基础

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

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

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

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

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

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

  • TCP 的三次握手与四次挥手过程是什么?
  • TCP 如何保证数据传输的可靠性?
  • HTTP 协议的基本原理和常用请求头有哪些?
  • OkHttp 的优点和适用场景是什么?
  • HTTPS 如何保证数据传输的安全性?
  • HTTP/2.0 相比 HTTP/1.1 新增了哪些内容?
  • MVVM 模式的特点和优势是什么?
  • MVP 模式的优缺点有哪些?
  • 常用的设计模式及其应用场景有哪些?
  • Java 中多态的表现和应用是什么?
  • 抽象类和接口的异同点有哪些?
  • 为什么要使用设计模式,其优势有哪些?
  • sleep () 和 wait () 的区别和应用场景是什么?
  • 多线程访问单例(双重锁写法)可能存在的问题及解决方法是什么?
  • Android 点击事件的分发机制是怎样的?
  • Android 中多线程如何处理,Handler 机制的作用是什么?
  • 自定义控件的基本流程和每一步具体做什么?
  • 多线程下载文件的实现方法和性能优化策略有哪些?
  • 断点续传文件的实现方法是什么?
  • Android 中的性能优化措施有哪些,包括布局和内存等方面?
  • Android 中出现内存泄漏的原因有哪些,如何发现和解决?
  • 解释 OOM(Out of Memory)的原因,当前应用可用内存为 20MB,已用 10MB,是否可能发生 OOM?
  • Android 中的跨进程通信方式有哪些,Binder 原理是什么,数据需要拷贝几次?
  • 使用 SharedPreferences 的 get 和 put 方法读写数据时可能面临的问题及 IO 性能方面的解决方法是什么?
  • 一个应用如何发现当前局域网中其他也开启了该应用的设备?
  • Android 中 CRASH 和 ANR 的区别是什么,ANR 是如何发生的,系统如何发现 ANR?
  • 普通 for 循环和增强 for 循环的区别及使用场景是什么?
  • 常用的排序算法及其时间、空间复杂度如何?
  • 图片压缩的方法有哪些?
  • 图片缓存如何实现,LruCache 算法的原理是什么?
  • HashMap 的原理和实现方式是什么?
  • JVM 的基本工作原理和常用配置有哪些?
  • String 类型数据在 JVM 中的存储区域是哪里?
  • MySQL 的相关知识点有哪些,包括基本操作、优化策略等?
  • Looper 是一个死循环,如何保证其性能?
  • Handler 如何延迟发送消息,其原理是什么?
  • 线程池的工作原理和使用场景是什么?
  • 当需要加载一个类时,在当前 ClassLoader 中没有找到该类会怎样(双亲委派),如果最后也没找到会抛出什么异常(ClassNotFound),抛出异常后会怎样处理?
  • 解释 AtomicReference 和 compareAndSet 的作用和应用场景。

去哪儿 面试

TCP 的三次握手与四次挥手过程是什么?

TCP(Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议 ,三次握手和四次挥手是其建立连接和断开连接的重要过程。

  • 三次握手过程
    • 第一次握手:客户端向服务器发送一个 SYN(同步序列号)包,其中包含客户端随机生成的初始序列号(ISN),此时客户端进入 SYN_SENT 状态,表示客户端希望与服务器建立连接。例如,客户端发送 SYN 包,序列号为 100,标志位 SYN=1。
    • 第二次握手:服务器收到客户端的 SYN 包后,会返回一个 SYN/ACK 包。这个包中,服务器会将客户端的 ISN 加 1 作为确认号 ACK 的值,表示对客户端的 SYN 的确认;同时,服务器也会随机生成自己的初始序列号,并将其放在 SYN 字段中,此时服务器进入 SYN_RCVD 状态。比如,服务器返回的 SYN/ACK 包,确认号 ACK=101,SYN=200。
    • 第三次握手:客户端收到服务器的 SYN/ACK 包后,会向服务器发送一个 ACK 包,确认号为服务器的 ISN 加 1,即 ACK=201,此时客户端进入 ESTABLISHED 状态,表示连接已成功建立。服务器收到客户端的 ACK 包后,也进入 ESTABLISHED 状态,至此,TCP 连接建立完成,可以开始进行数据传输。

  • 四次挥手过程
    • 第一次挥手:主动关闭方(通常是客户端)发送一个 FIN(结束标志)包,表示客户端不再发送数据,但仍可以接收数据,此时客户端进入 FIN_WAIT_1 状态。
    • 第二次挥手:被动关闭方(通常是服务器)收到 FIN 包后,会发送一个 ACK 包,表示已经收到客户端的关闭请求,此时服务器进入 CLOSE_WAIT 状态,客户端收到 ACK 包后进入 FIN_WAIT_2 状态。
    • 第三次挥手:服务器发送一个 FIN 包给客户端,表示服务器也准备关闭连接,此时服务器进入 LAST_ACK 状态。
    • 第四次挥手:客户端收到服务器的 FIN 包后,发送一个 ACK 包给服务器,确认关闭连接,客户端进入 TIME_WAIT 状态,等待 2MSL(最长报文段寿命)时间后进入 CLOSED 状态。服务器收到 ACK 包后,直接进入 CLOSED 状态,连接彻底关闭。

TCP 如何保证数据传输的可靠性?

TCP 通过多种机制来确保数据传输的可靠性,以下是一些主要的方面:

  • 序列号与确认应答机制:TCP 为每个发送的字节都编上一个序列号。接收方收到数据后,会向发送方发送确认应答(ACK),其中包含了期望收到的下一个字节的序列号。发送方通过确认应答来确定哪些数据已经被对方成功接收,哪些数据需要重发。例如,如果发送方发送了序列号为 100-200 的数据段,接收方返回的 ACK 为 201,表示序列号 100-200 的数据已正确接收,发送方可以继续发送下一个数据段。如果发送方在一定时间内没有收到 ACK,就会认为数据丢失,从而进行重传。
  • 重传机制:当发送方发现数据丢失或未被正确接收时,会自动进行重传。有两种常见的重传方式,一种是基于超时重传,即发送方在发送数据后启动一个定时器,如果在定时器超时前没有收到确认应答,就会重传该数据段;另一种是快速重传,当接收方收到乱序的数据包时,会立即向发送方发送重复的 ACK,发送方收到一定数量的重复 ACK 后,就会意识到前面的数据可能丢失,从而立即进行重传,而不需要等待定时器超时,这样可以更快地恢复丢失的数据。
  • 流量控制机制:为了避免发送方发送数据过快,导致接收方缓冲区溢出,TCP 使用流量控制机制。接收方会在确认应答中告知发送方自己的接收窗口大小,发送方根据接收窗口的大小来调整发送数据的速率,确保接收方能够及时处理收到的数据,防止数据丢失。
  • 拥塞控制机制:TCP 还具备拥塞控制功能,以避免网络出现拥塞。在网络状况良好时,TCP 会逐渐增加发送的数据量;而当发现网络出现拥塞时,如出现丢包现象,TCP 会迅速减少发送的数据量,以缓解网络拥塞。通过这种方式,TCP 可以在网络资源有限的情况下,合理地分配带宽,保证数据传输的可靠性和稳定性。

HTTP 协议的基本原理和常用请求头有哪些?

HTTP(HyperText Transfer Protocol)是一种用于分布式、协作式和超媒体信息系统的应用层协议,它是互联网上信息传递与共享的重要基础。

  • 基本原理
    • 请求与响应模型:HTTP 基于请求 / 响应模型工作。客户端(如浏览器)向服务器发送一个 HTTP 请求,请求中包含了请求方法(如 GET、POST 等)、请求的 URL(统一资源定位符)以及一些请求头信息等。服务器收到请求后,根据请求的内容进行相应的处理,并返回一个 HTTP 响应给客户端。响应中包含了状态码、响应头信息和响应体,状态码用于表示请求的处理结果,如 200 表示成功,404 表示未找到资源等;响应体则包含了客户端所请求的实际数据,如网页的 HTML 内容、图片数据等。
    • 无状态性:HTTP 是无状态的协议,这意味着每个请求都是独立的,服务器不会保留之前请求的任何信息。虽然这种无状态性简化了服务器的设计和实现,但在某些需要记住用户状态的应用场景中,通常需要借助 Cookie 或 Session 等技术来实现状态管理。
  • 常用请求头
    • User-Agent:用于标识客户端的信息,如浏览器的名称、版本号以及操作系统等。服务器可以根据 User-Agent 头来判断客户端的类型,以便返回适合该客户端的内容。例如,"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" 表示这是一个使用 Windows 10 操作系统,Chrome 91.0.4472.124 浏览器的客户端。
    • Accept:用于告知服务器客户端能够接受的内容类型。客户端可以在请求头中指定它能够理解和处理的各种数据格式,如 "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8",表示客户端优先接受 HTML 和 XHTML 格式的数据,也可以接受其他格式的数据,但优先级较低。
    • Content-Type:当客户端向服务器发送数据时,用于指定发送的数据的类型。例如,在使用 POST 方法提交表单数据时,通常会设置 "Content-Type: application/x-www-form-urlencoded" ,表示数据是经过 URL 编码的表单数据;如果是上传文件,则可能会设置 "Content-Type: multipart/form-data" 。
    • Cookie:用于在客户端和服务器之间传递一些状态信息。客户端在发送请求时,会将之前从服务器获取的 Cookie 信息一起发送给服务器,服务器可以根据 Cookie 中的信息来识别客户端的身份、会话状态等,从而提供个性化的服务。
    • Authorization:用于向服务器提供身份验证信息,通常在需要访问受保护资源时使用。例如,在进行基本认证时,客户端会在请求头中添加 "Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=",其中 "Basic" 表示使用基本认证方式,后面的字符串是经过 Base64 编码的用户名和密码。

OkHttp 的优点和适用场景是什么?

OkHttp 是一个高效的开源 HTTP 客户端库,在 Android 开发及其他 Java 应用中广泛使用,具有众多优点和适用场景。

  • 优点
    • 高效性:OkHttp 内部实现了连接池,对于同一主机的多个请求可以复用连接,减少了连接建立和关闭的开销,大大提高了性能和效率。同时,它还支持 HTTP/2 协议,相比 HTTP/1.1,HTTP/2 在性能上有显著提升,如多路复用、头部压缩等特性,使得数据传输更加快速和高效。
    • 易用性:OkHttp 提供了简洁直观的 API,使用起来非常方便。它对 HTTP 请求和响应的处理进行了良好的封装,开发人员可以轻松地构建请求、设置请求头、添加请求参数等,并且能够方便地获取响应数据和处理响应状态。
    • 可靠性:OkHttp 具备强大的错误处理和恢复机制。它能够自动处理一些常见的网络错误,如连接超时、DNS 解析失败等,并提供了相应的回调方法,让开发人员可以根据具体情况进行错误处理和恢复操作。同时,它还支持自动重连和缓存功能,进一步增强了数据传输的可靠性。
    • 扩展性:OkHttp 具有良好的扩展性,它允许开发人员通过拦截器机制对请求和响应进行拦截和处理。开发人员可以自定义拦截器,实现诸如添加公共请求头、日志记录、缓存控制、请求重试等功能,方便地对网络请求进行定制和扩展。
  • 适用场景
    • 移动应用开发:在 Android 和 iOS 等移动应用开发中,OkHttp 是非常受欢迎的网络库。它可以方便地与各种后端服务器进行通信,高效地获取和提交数据,满足移动应用对网络性能和稳定性的要求。例如,在一个社交应用中,使用 OkHttp 可以快速地加载用户的好友列表、动态消息等数据。
    • 后端服务开发:不仅在客户端,OkHttp 在后端服务开发中也有广泛的应用。它可以作为一个轻量级的 HTTP 客户端,用于服务与服务之间的通信,如微服务架构中的服务调用。通过 OkHttp,不同的微服务可以方便地相互协作,实现业务功能的整合和数据的共享。
    • 网络爬虫开发:对于需要从网络上抓取数据的网络爬虫应用,OkHttp 也是一个不错的选择。它可以高效地发送 HTTP 请求,获取网页内容,并通过其强大的 API 方便地对响应数据进行解析和处理。同时,通过自定义拦截器,还可以实现一些爬虫的高级功能,如设置请求频率、处理登录验证等。

HTTPS 如何保证数据传输的安全性?

HTTPS(HyperText Transfer Protocol Secure)是在 HTTP 的基础上加入了 SSL/TLS 加密协议,通过多种技术手段来确保数据传输的安全性。

  • 加密算法的应用:HTTPS 使用了对称加密和非对称加密相结合的方式来保护数据的机密性。在连接建立初期,客户端和服务器通过非对称加密算法进行密钥交换。服务器将自己的公钥发送给客户端,客户端使用该公钥对生成的对称加密密钥进行加密,然后发送给服务器。服务器使用自己的私钥解密得到对称加密密钥。之后,双方使用这个对称加密密钥对数据进行加密和解密,由于对称加密算法的效率较高,这样可以保证数据在传输过程中的高效加密和解密,防止数据被窃取或篡改。
  • 数字证书的验证:服务器端会向权威的数字证书颁发机构(CA)申请数字证书。数字证书包含了服务器的公钥以及服务器的一些身份信息,如域名等。客户端在与服务器建立连接时,会首先验证服务器的数字证书。客户端会检查证书的合法性,包括证书是否由信任的 CA 颁发、证书是否过期、证书的域名与服务器的实际域名是否匹配等。通过数字证书的验证,可以确保客户端连接的是真实可靠的服务器,防止中间人攻击。
  • 数据完整性校验:HTTPS 通过消息认证码(MAC)或数字签名等技术来保证数据的完整性。在数据传输过程中,发送方会对数据进行哈希运算,生成一个消息摘要,并将其与数据一起发送给接收方。接收方收到数据后,也会对数据进行相同的哈希运算,并将得到的消息摘要与发送方发送的进行对比。如果两者一致,则说明数据在传输过程中没有被篡改;否则,说明数据已被篡改,接收方可以拒绝接收该数据。
  • 安全的连接建立与协商:HTTPS 在连接建立阶段通过 SSL/TLS 协议的握手过程来协商加密算法、密钥交换等参数,确保双方使用相同的安全参数进行通信。在握手过程中,客户端和服务器会互相发送一些信息,如支持的加密算法列表、随机数等,通过一系列的消息交互和验证,最终建立起一个安全的连接通道,只有通过身份验证且拥有正确密钥的双方才能在这个通道上进行数据传输,从而保证了数据传输的安全性和可靠性。

HTTP/2.0 相比 HTTP/1.1 新增了哪些内容?

HTTP/2.0 在 HTTP/1.1 的基础上进行了诸多改进和新增内容。首先,它采用了二进制分帧层,将所有传输的信息分割为更小的帧,这些帧可以交错发送,极大地提高了传输效率和并发性。与 HTTP/1.1 的文本格式相比,二进制格式更易于解析和处理,减少了解析错误。

在多路复用方面,HTTP/2.0 允许在同一个连接上同时发送多个请求和响应,而不需要像 HTTP/1.1 那样依赖多个连接来实现并发。这样可以避免因频繁创建和销毁连接带来的性能开销,同时也能更好地利用网络带宽。

头部压缩也是 HTTP/2.0 的重要特性之一。它使用 HPACK 算法对请求和响应头进行压缩,减少了头部数据的传输量,尤其是在请求头信息较多的情况下,能显著提升性能。

此外,HTTP/2.0 还增强了服务器推送功能。服务器可以根据客户端的需求,主动向客户端推送相关的资源,如 CSS、JavaScript 文件等,而不需要客户端再次发起请求,进一步加快了页面的加载速度。

在流量控制上,HTTP/2.0 提供了更精细的机制,允许客户端和服务器对每个流或整个连接进行流量控制,确保数据的平稳传输,避免某一方发送数据过快导致接收方处理不过来的情况。

MVVM 模式的特点和优势是什么?

MVVM 模式即 Model-View-ViewModel 模式,具有以下特点和优势。

其一是数据绑定特性,它实现了 View 层和 ViewModel 层的双向数据绑定。这意味着当 ViewModel 中的数据发生变化时,View 层会自动更新相应的显示,反之亦然。例如,在一个用户注册界面,用户在输入框中输入用户名,ViewModel 中对应的用户名数据会实时更新,同时如果 ViewModel 中的用户名数据因其他逻辑被修改,输入框中的显示也会自动改变,极大地简化了代码逻辑,提高了开发效率。

其二,ViewModel 层的独立性是一大特点。它将业务逻辑和视图逻辑分离,使得 ViewModel 可以独立于 View 进行单元测试。开发人员可以专注于业务逻辑的实现,而不用担心与具体的视图展示相互干扰。比如,对于一个商品列表的 ViewModel,它可以单独进行数据获取、数据处理等逻辑的测试,而不依赖于具体的 ListView 或 RecyclerView 等视图组件。

再者,MVVM 模式提高了代码的可维护性和可扩展性。由于各层职责明确,当需求变更时,比如需要增加新的业务功能或修改视图展示方式,只需要在相应的层进行修改,不会对其他层造成大面积的影响。以一个电商应用为例,若要增加商品的筛选功能,只需在 ViewModel 中添加相应的筛选逻辑,View 层只需进行少量的布局调整,而 Model 层的数据获取和存储逻辑基本无需变动。

另外,它还支持团队协作开发。不同的开发人员可以分别专注于 Model、View 和 ViewModel 层的开发,并行工作,提高了开发进度和代码质量。比如,后端开发人员专注于 Model 层的数据处理和接口提供,前端开发人员专注于 View 层的界面设计和交互,而中间的 ViewModel 层则由熟悉业务逻辑的开发人员来实现和维护。

MVP 模式的优缺点有哪些?

MVP 模式即 Model-View-Presenter 模式,有其自身的优点和缺点。

优点方面,首先是分离了视图逻辑和业务逻辑。View 层只负责展示界面和接收用户交互,而 Presenter 层处理所有的业务逻辑和数据操作,并将处理结果反馈给 View 层进行展示。这样使得代码结构更加清晰,易于理解和维护。例如在一个新闻客户端中,View 层只负责展示新闻列表和新闻详情页面的布局和交互,而 Presenter 层负责从网络获取新闻数据、进行数据解析和处理等业务逻辑,当需要修改新闻展示样式时,只需要在 View 层修改,而不影响业务逻辑。

其次,MVP 模式便于进行单元测试。由于 Presenter 层不依赖于具体的 View 实现,它可以通过模拟 View 层的接口来进行独立的单元测试,从而更好地保证代码的质量。比如,可以编写测试用例来验证 Presenter 层对数据的处理和传递是否正确,而不需要启动整个应用程序。

再者,它增强了代码的可扩展性和可维护性。当应用的功能增加或需求变更时,可以方便地在 Presenter 层添加或修改业务逻辑,而不会对 View 层和 Model 层造成太大的影响。以一个音乐播放应用为例,若要增加新的音乐播放模式,只需在 Presenter 层添加相应的逻辑,View 层和 Model 层的改动相对较小。

然而,MVP 模式也存在一些缺点。一是代码量相对较多,因为需要定义大量的接口和回调方法来实现 View 层和 Presenter 层之间的交互,尤其是在复杂的界面中,会使得代码变得冗长。

二是存在一定的学习成本,对于初学者来说,理解和掌握 MVP 模式的三层结构以及它们之间的交互关系需要一定的时间和实践。

三是 View 层和 Presenter 层之间的交互可能会比较复杂,当界面有大量的用户交互和数据更新时,需要精心设计接口和回调方法,否则容易导致代码的混乱和难以维护。

常用的设计模式及其应用场景有哪些?

以下是一些常用的设计模式及其应用场景:

  • 单例模式:其特点是确保一个类只有一个实例,并提供一个全局访问点。应用场景非常广泛,比如在 Android 中,数据库连接对象通常使用单例模式。因为在整个应用程序的生命周期内,一般只需要一个数据库连接实例来进行数据的读写操作。如果创建多个数据库连接实例,不仅会浪费系统资源,还可能导致数据不一致等问题。再如,应用程序中的配置文件管理类也常采用单例模式,方便在不同的模块中统一获取和修改配置信息。
  • 工厂模式:它将对象的创建和使用分离。工厂类负责创建对象,而客户端只需要使用对象,无需关心对象的创建过程。例如,在一个游戏开发中,游戏中的各种角色可以通过工厂模式来创建。不同类型的角色,如战士、法师等,有不同的属性和行为,通过工厂类可以根据游戏的需求动态地创建不同类型的角色,而不是在客户端代码中直接实例化角色对象,这样提高了代码的可维护性和可扩展性。如果后续要添加新的角色类型,只需要在工厂类中进行修改,而不影响客户端的使用。
  • 观察者模式:定义了一种一对多的依赖关系,当一个对象的状态发生变化时,所有依赖于它的对象都会收到通知并自动更新。在 Android 开发中,典型的应用是 ListView 和 Adapter 的配合。当 Adapter 中的数据发生变化时,ListView 会自动更新界面显示。Adapter 作为被观察者,当它的数据集合发生添加、删除或修改操作时,会通知 ListView 等观察者进行相应的更新。同样,在一些事件总线框架中,如 EventBus,也是基于观察者模式实现的,不同的组件可以注册成为观察者,当有事件发生时,发布者会通知所有注册的观察者进行相应的处理。
  • 策略模式:它定义了一系列算法,将每个算法封装起来,并使它们可以相互替换。在 Android 中的动画效果实现中可以使用策略模式。例如,实现一个图片切换动画,可以有淡入淡出、滑动、缩放等不同的动画策略。通过策略模式,可以将每种动画策略封装成一个具体的类,然后根据用户的选择或应用的需求动态地切换动画策略,而不需要修改使用动画的客户端代码,提高了代码的灵活性和可维护性。
  • 建造者模式:用于创建复杂对象,将对象的构建过程和表示分离。在 Android 中,创建一个复杂的自定义 View 时可以使用建造者模式。比如创建一个具有多种样式和属性的图表 View,它可能有不同的颜色、线条样式、数据显示方式等属性。通过建造者模式,可以逐步设置这些属性,最后构建出完整的图表 View,而不是在构造函数中一次性传入所有的属性,这样使得代码更加清晰和易于维护,也方便了对象的创建和使用。

Java 中多态的表现和应用是什么?

在 Java 中,多态主要表现为两种形式,即方法的重载和方法的重写。

方法的重载是指在同一个类中,有多个方法具有相同的名字,但参数列表不同。参数列表的不同可以体现在参数的个数、类型或顺序上。例如,在一个数学计算类中,可以有多个名为 add 的方法,一个接收两个整数参数用于计算整数相加,另一个接收两个浮点数参数用于计算浮点数相加。当调用 add 方法时,Java 编译器会根据传入的参数类型和个数来自动匹配对应的方法,这体现了多态性。通过方法重载,可以让代码更加简洁和易读,提高了代码的复用性。开发人员可以根据不同的参数需求提供不同的实现,而不需要为每个不同的操作定义不同的方法名。

方法的重写发生在子类和父类之间。当子类继承父类时,可以重写父类中的方法,以实现不同于父类的行为。例如,在一个图形类层次结构中,父类 Shape 有一个 draw 方法,子类 Circle 和 Rectangle 分别重写了这个 draw 方法,以实现各自不同的绘制逻辑。当通过父类引用调用 draw 方法时,会根据对象的实际类型来执行相应子类的重写方法。这使得代码更加灵活和可扩展。比如在一个绘图应用中,可以创建一个图形数组,数组中存储不同类型的图形对象,通过遍历数组并调用 draw 方法,就可以根据每个图形对象的实际类型来执行相应的绘制逻辑,而不需要为每种图形编写单独的绘制代码。

多态的应用非常广泛。在面向对象的软件设计中,它有助于提高代码的可维护性和可扩展性。以一个简单的动物叫声模拟系统为例,有动物类 Animal 及其子类 Dog、Cat 等。Animal 类中有一个 makeSound 方法,Dog 和 Cat 子类分别重写了这个方法来发出各自独特的叫声。在主程序中,可以创建一个 Animal 类型的数组,将不同的动物对象存储在其中,然后通过遍历数组调用 makeSound 方法,就可以根据每个动物对象的实际类型来发出相应的叫声。这样,当需要添加新的动物类型时,只需要创建新的子类并重写 makeSound 方法,而不需要修改主程序中调用 makeSound 方法的代码,大大降低了代码的修改和维护成本。

在框架和库的设计中,多态也发挥着重要作用。许多 Java 框架利用多态来实现插件式的架构,允许用户通过实现特定的接口或重写特定的方法来扩展框架的功能。例如,在 Java 的 Servlet 规范中,开发人员可以通过创建自己的 Servlet 类并重写 service 方法来处理不同的 HTTP 请求,而 Servlet 容器会根据请求的 URL 来调用相应 Servlet 的 service 方法,这就是基于多态的机制实现的。

抽象类和接口的异同点有哪些?

抽象类是一种不能被实例化的类,它通常用于为一组子类提供通用的属性和方法。接口则是一种抽象类型,它定义了一组方法签名,但不包含方法的实现。

相同点:两者都不能直接被实例化,都用于定义一组规范或契约,让具体的类去实现或继承。

不同点:

  • 语法形式:抽象类使用 abstract 关键字修饰,其中可以包含抽象方法和非抽象方法。接口使用 interface 关键字定义,其中的方法默认都是 public abstract ,并且不能包含非抽象方法,从 Java 8 开始接口中可以有默认方法和静态方法 。
  • 实现方式:一个类只能继承一个抽象类,但可以实现多个接口。例如,class Dog extends Animal implements Runnable, Serializable,这里 Animal 是抽象类,Runnable 和 Serializable 是接口。
  • 设计目的:抽象类主要用于代码的复用和对事物本质特征的抽象,它更侧重于对一组相关类的通用行为和属性的提取和封装。接口则更侧重于定义一组行为规范,用于不同类之间的协作和通信,它强调的是实现类必须遵循的契约。
  • 方法实现:抽象类中的抽象方法必须在子类中被实现,非抽象方法可以被子类继承和重写。接口中的所有方法都必须在实现类中被实现。
  • 状态维护:抽象类可以有成员变量来维护状态,这些变量可以被子类继承和使用。接口通常不包含成员变量,从 Java 9 开始接口可以有私有方法和私有静态方法,但这些方法主要是为了辅助接口中的其他方法实现,而不是用于维护状态。

为什么要使用设计模式,其优势有哪些?

设计模式是软件开发中针对反复出现的问题所总结归纳出的通用解决方案。

使用设计模式的原因:

  • 应对复杂需求:在软件开发过程中,随着项目规模的扩大和功能的增加,软件系统会变得越来越复杂。设计模式提供了一种结构化的方式来组织代码,帮助开发者更好地理解和管理复杂的系统结构,将复杂的问题分解为更小的、更易于管理的子问题。
  • 提高代码复用性:许多设计模式都强调了代码的复用性。通过将通用的功能和行为封装在特定的设计模式中,可以在不同的项目或模块中重复使用这些代码,避免了重复编写相似的代码,从而提高了开发效率,减少了代码量和维护成本。
  • 增强可维护性:当软件系统需要进行修改和扩展时,良好的设计模式可以使代码更容易理解和修改。遵循设计模式的代码结构通常具有更好的可读性和可维护性,新的开发人员能够更快地理解代码的功能和意图,降低了维护的难度和风险。
  • 实现软件的可扩展性:软件系统需要不断地发展和演进以满足新的需求。设计模式为软件的扩展性提供了支持,使得系统能够更容易地添加新功能、修改现有功能或适应新的业务规则,而不会对整个系统的结构造成太大的影响。

设计模式的优势:

  • 促进团队协作:设计模式提供了一种通用的语言和规范,使得开发团队成员之间能够更好地沟通和协作。当大家都熟悉并遵循相同的设计模式时,能够更容易地理解彼此的代码,减少了沟通成本和误解,提高了团队的工作效率。
  • 提升软件质量:合理地运用设计模式可以使软件系统更加健壮、稳定和可靠。设计模式考虑了软件设计中的各种因素,如灵活性、可扩展性、可维护性等,有助于避免一些常见的设计缺陷和错误,从而提升了软件的整体质量。
  • 适应变化的需求:在软件开发过程中,需求的变化是不可避免的。设计模式的灵活性和可扩展性使得软件系统能够更好地应对需求的变化,减少了因需求变更而导致的系统重构和大规模修改的可能性。

sleep () 和 wait () 的区别和应用场景是什么?

在 Java 中,sleep() 和 wait() 都可以用于暂停线程的执行,但它们有一些重要的区别。

区别:

  • 所属类不同:sleep() 是 Thread 类的静态方法,而 wait() 是 Object 类的方法。这意味着 sleep() 可以直接通过线程类调用,而 wait() 必须在同步块中通过对象调用。
  • 是否释放锁:sleep() 方法在暂停线程执行时,不会释放线程所占用的锁。而 wait() 方法会释放线程占用的锁,使得其他线程有机会获取该锁并执行同步块中的代码。
  • 唤醒方式:sleep() 方法在指定的时间到达后会自动唤醒线程继续执行。而 wait() 方法需要通过 notify() 或 notifyAll() 方法来唤醒,并且通常是在其他线程中调用这两个方法来通知等待的线程。
  • 使用场景和目的:sleep() 通常用于暂停线程一段时间,以实现简单的定时任务或控制线程的执行频率,不涉及线程间的通信和同步。而 wait() 主要用于线程间的通信和协作,当一个线程需要等待某个条件满足时,可以调用 wait() 方法进入等待状态,直到其他线程通过 notify() 或 notifyAll() 方法通知它条件已经满足。

应用场景:

  • sleep () 的应用场景:
    • 模拟延迟操作:例如,在一个游戏中,需要每隔一段时间生成一个新的怪物,可以使用 sleep() 方法来暂停主线程,等待一段时间后再创建怪物对象。
    • 控制循环执行频率:在一个循环中,如果需要每隔一段时间执行一次循环体中的代码,可以在循环体内使用 sleep() 方法来实现。
  • wait () 的应用场景:
    • 生产者 - 消费者模型:生产者线程生产数据并将其放入共享缓冲区,消费者线程从缓冲区中取出数据进行处理。当缓冲区为空时,消费者线程调用 wait() 方法等待生产者生产数据;当生产者生产了数据后,通过 notify() 或 notifyAll() 方法唤醒消费者线程。
    • 线程池实现:线程池中的线程在没有任务可执行时,可以调用 wait() 方法进入等待状态,当有新的任务提交到线程池时,通过 notify() 或 notifyAll() 方法唤醒等待的线程来执行任务。

多线程访问单例(双重锁写法)可能存在的问题及解决方法是什么?

双重锁写法是一种创建单例模式的常见方式,旨在实现延迟加载和高效的多线程访问控制。

可能存在的问题:

  • 指令重排序问题:在双重锁写法中,由于 Java 编译器和处理器的指令重排序优化,可能导致在对象实例化过程中,对象引用先被赋值,但对象的初始化过程尚未完成。这就可能使得其他线程在获取单例对象时,得到的是一个尚未完全初始化的对象,从而引发程序错误。

解决方法:

  • 使用 volatile 关键字:通过在单例对象的引用前添加 volatile 关键字,可以禁止指令重排序。这样就确保了对象的初始化过程在对象引用被赋值之前完成,从而保证了其他线程获取到的单例对象是完全初始化的。

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

Android 的点击事件分发机制主要涉及三个重要的方法:dispatchTouchEvent()、onInterceptTouchEvent() 和 onTouchEvent() 。

  • dispatchTouchEvent() 方法:该方法用于分发点击事件,它会按照一定的顺序将事件传递给当前视图及其子视图。当一个点击事件发生时,首先会调用当前活动的 dispatchTouchEvent() 方法,然后它会根据事件的类型和视图的层次结构来决定如何分发事件。
  • onInterceptTouchEvent() 方法:此方法用于拦截点击事件,通常在视图容器类中使用,如 ViewGroup 。它的作用是在事件传递给子视图之前,判断是否要拦截该事件。如果该方法返回 true ,则表示拦截事件,事件将不再传递给子视图,而是由当前视图自己处理;如果返回 false ,则事件会继续传递给子视图。
  • onTouchEvent() 方法:这个方法用于处理点击事件,当事件传递到某个视图时,如果该视图没有被拦截且它的 onTouchEvent() 方法返回 true ,则表示该视图消费了这个事件,事件将不再向上传递;如果返回 false ,则事件会继续向上传递给父视图的 onTouchEvent() 方法,直到被处理或者传递到顶层视图。

事件分发的大致流程如下:当一个点击事件发生时,首先会从顶层的 Activity 的 dispatchTouchEvent() 方法开始分发。如果 Activity 没有拦截事件,它会将事件传递给根视图 ViewGroup 的 dispatchTouchEvent() 方法。在 ViewGroup 中,如果 onInterceptTouchEvent() 方法返回 false ,则继续将事件传递给子视图的 dispatchTouchEvent() 方法,如此递归传递,直到找到能够处理该事件的视图为止。当子视图处理完事件后,如果事件还有剩余的未处理部分,如触摸移动或抬起事件,这些事件会按照相反的顺序依次向上传递给父视图的 onTouchEvent() 方法进行处理,直到回到 Activity 。

在整个点击事件分发过程中,每个视图都有机会参与事件的处理和传递,通过合理地重写这三个方法,可以实现各种复杂的触摸交互效果和事件处理逻辑。

Android 中多线程如何处理,Handler 机制的作用是什么?

在 Android 中,多线程处理主要用于避免主线程(UI 线程)执行耗时操作导致界面卡顿。常见的处理方式有以下几种:

  • 使用 Thread 类:直接创建 Thread 的子类并重写 run 方法,在 run 方法中编写耗时任务。但这种方式如果需要更新 UI,需通过 Handler 等机制切换到主线程,因为 Android 规定只有主线程能更新 UI。
  • 使用 Runnable 接口:创建 Runnable 接口的实现类,将其作为参数传递给 Thread 的构造函数,然后启动线程。这种方式使得代码逻辑与线程的创建和执行分离,更具灵活性。
  • 使用 AsyncTask 类:它是对 Thread 和 Handler 的封装,便于在后台线程执行任务并在主线程更新 UI。通过重写其 doInBackground、onPostExecute 等方法,可以方便地实现后台任务的执行和结果的返回与显示。

Handler 机制在 Android 多线程处理中起着关键作用。它主要用于在不同线程之间进行消息传递和通信。Handler 与 Looper 和 MessageQueue 配合工作,一个线程可以有一个 Looper,它负责不断从 MessageQueue 中取出消息,而 Handler 则用于发送和处理这些消息。当子线程需要更新 UI 时,可通过 Handler 向主线程的 MessageQueue 发送消息,主线程的 Looper 取出消息后,由对应的 Handler 进行处理,从而实现了在主线程更新 UI 的目的。此外,Handler 还可用于在不同的工作线程之间传递消息和协调工作,使得多线程的协作更加有序和高效 。

自定义控件的基本流程和每一步具体做什么?

自定义控件在 Android 开发中非常重要,它能满足特定的 UI 需求。基本流程如下:

第一步:确定需求和功能 明确自定义控件要实现的功能和外观。例如,要创建一个圆形的 ImageView,或者一个具有特殊动画效果的按钮等。这一步是基础,后续的所有工作都围绕此展开。

第二步:继承合适的父类 根据需求选择合适的父类继承。如果是简单的组合已有控件,可继承 ViewGroup 或其子类;如果是完全自定义绘制外观,可继承 View 类。比如,自定义一个仪表盘控件,可能继承 View 类更合适,以便完全掌控绘制过程。

第三步:添加属性和样式支持 定义自定义控件的属性,以便在布局文件中方便地设置其外观和行为。可通过在 res/values/attrs.xml 文件中声明属性,然后在构造函数中获取并应用这些属性值。例如,为自定义的圆形 ImageView 定义半径、边框颜色等属性。

第四步:测量控件大小 重写 onMeasure 方法,根据控件的内容和布局要求计算控件的大小。需要考虑父容器的约束和自身的内容需求,以确保控件在不同布局中能正确显示大小。

第五步:绘制控件 重写 onDraw 方法,使用 Android 的绘图 API 在画布上绘制控件的外观。根据需求绘制各种图形、文本等。如绘制圆形 ImageView 时,使用 Canvas 的 drawCircle 方法绘制圆形。

第六步:处理事件 根据控件的功能,处理各种触摸、按键等事件。重写相应的事件处理方法,如 onTouchEvent 方法,以实现与用户的交互。例如,自定义按钮需要处理点击事件,执行相应的业务逻辑。

第七步:在布局文件中使用 完成自定义控件的开发后,就可以在布局文件中像使用普通控件一样使用它,通过设置属性来定制其外观和行为。

多线程下载文件的实现方法和性能优化策略有哪些?

多线程下载文件的实现方法

  • 使用传统的 Thread 和 Runnable:创建多个线程,每个线程负责下载文件的一部分。通过计算文件大小和线程数量,确定每个线程下载的起始位置和结束位置。例如,有一个 100MB 的文件,使用 5 个线程下载,每个线程负责下载 20MB 的片段。在每个线程的 run 方法中,通过 HTTP 的 Range 头字段指定下载范围,然后将下载的数据写入本地文件的相应位置。
  • 使用线程池:创建一个线程池,将下载任务提交给线程池执行。线程池可以管理线程的创建和销毁,提高性能和资源利用率。可使用 Java 的 ExecutorService 和相关的线程池实现类,如 ThreadPoolExecutor。通过调用线程池的 execute 方法提交下载任务,任务实现 Runnable 接口,在其中编写下载逻辑。
  • 使用第三方库:如 OkHttp 等,它提供了方便的多线程下载功能。可通过配置 OkHttp 的拦截器等,实现多线程下载和文件的合并等操作。

性能优化策略

  • 合理选择线程数量:线程数量不宜过多,过多可能导致系统资源竞争激烈,反而降低性能。一般根据网络状况和设备性能来确定,如根据网络带宽和文件大小计算出一个合适的线程数量,以充分利用网络资源又不造成过度竞争。
  • 缓存策略:对已下载的文件片段进行缓存,避免重复下载。可在本地开辟一块缓存区域,将下载的文件片段临时存储,下次需要时直接从缓存中读取,减少网络请求。
  • 错误处理和重试机制:在下载过程中可能会出现网络异常等错误,建立错误处理和重试机制。当出现错误时,根据错误类型进行相应处理,如网络超时可尝试重新连接和下载一定次数,以提高下载的成功率。
  • 优化网络请求:减少不必要的网络请求头信息,设置合理的超时时间等。例如,只发送必要的 HTTP 头字段,避免发送大量冗余信息,根据网络状况设置合适的连接超时和读取超时时间,以提高网络请求的效率。

断点续传文件的实现方法是什么?

断点续传是指在文件下载或上传过程中,能够在中断的位置继续进行传输,而不是重新开始。其实现方法主要有以下几个关键步骤:

记录断点信息 首先需要记录文件下载或上传的断点位置。这可以通过在本地存储一个文件或使用数据库来实现。每次传输开始时,读取上次的断点位置,然后从该位置继续传输。例如,将断点位置信息存储在一个文本文件中,文件中记录已下载或已上传的字节数。

设置请求头 在 HTTP 请求中,通过设置特定的请求头来告知服务器从断点位置开始传输。常用的是 Range 头字段,格式为 Range: bytes=startPos-endPos,表示请求从 startPos 字节到 endPos 字节的数据。服务器根据这个请求头,返回指定范围的数据。

处理服务器响应 服务器根据 Range 头字段返回相应的数据段,客户端需要正确处理这些数据段。将接收到的数据段写入本地文件的相应位置,而不是覆盖整个文件。例如,使用 RandomAccessFile 类,它可以指定写入文件的位置,将接收到的数据追加到文件的断点之后。

更新断点信息 在每次成功传输一段数据后,需要更新断点位置信息。将新的断点位置写入本地存储的文件或数据库中,以便下次继续传输时能准确从该位置开始。

错误处理和完整性检查 在传输过程中可能会出现各种错误,如网络中断等。需要建立错误处理机制,当出现错误时,根据错误类型进行相应处理,如尝试重新连接和继续传输。同时,在文件传输完成后,要进行完整性检查,可通过计算文件的哈希值等方法,与服务器端的文件哈希值进行对比,确保文件完整无误地传输。

Android 中的性能优化措施有哪些,包括布局和内存等方面?

布局优化

  • 减少布局层级:尽量使用扁平化的布局结构,避免过多的嵌套。因为每一层布局都会增加测量、布局和绘制的时间。例如,使用 RelativeLayout 和 LinearLayout 的合理组合,代替多层嵌套的 LinearLayout。如果一个界面有很多层 LinearLayout 嵌套,可考虑使用 ConstraintLayout 来减少层级。
  • 使用合适的布局容器:根据界面需求选择性能更好的布局容器。如 RecyclerView 在处理大量列表数据时性能优于 ListView,因为它采用了更高效的回收复用机制。对于复杂的页面布局,可使用 Fragment 来进行模块化管理,提高布局的可维护性和性能。
  • 避免过度绘制:减少不必要的背景绘制和重叠绘制。检查布局中是否有被遮挡但仍在绘制的视图,去除不必要的背景设置。例如,一个 ListView 的每个 Item 都有自己的背景,而父容器也有背景,可能会导致过度绘制,可根据实际情况合理设置背景。

内存优化

  • 对象的合理创建和复用:避免频繁创建和销毁对象,尽量复用已有的对象。如在列表滚动时,使用 ViewHolder 模式在 RecyclerView 中复用视图对象,减少对象创建的开销。对于一些频繁使用的工具类对象,可采用单例模式,确保只有一个实例存在,避免重复创建。
  • 及时释放内存:当不再使用的对象时,及时将其引用置为 null,以便垃圾回收器回收内存。例如,在 Activity 销毁时,释放其中不再使用的资源,如关闭数据库连接、停止正在播放的音乐等。对于一些占用内存较大的对象,如 Bitmap,要确保在使用完后正确释放内存,可使用 Bitmap 的 recycle 方法等。
  • 内存泄漏的避免:注意避免内存泄漏,常见的如忘记注销广播接收器、持有不必要的静态引用等。在 Activity 或 Fragment 的生命周期方法中,正确地注册和注销广播接收器,避免在静态变量中持有 Activity 或 Fragment 的引用,防止其无法被回收。

其他方面优化

  • 图片优化:使用合适的图片格式和压缩技术。如 WebP 格式的图片在保证质量的同时,文件大小通常比 JPEG 等格式更小。根据图片的显示需求,进行合理的压缩,避免加载过大的图片导致内存占用过高和加载缓慢。
  • 代码优化:避免在主线程执行耗时操作,将耗时任务放在子线程中执行。优化算法和数据结构,提高代码的执行效率。例如,在处理大量数据的排序和搜索时,选择合适的算法,如快速排序等,可大大提高性能。同时,减少不必要的代码逻辑和冗余计算,提高代码的简洁性和性能。

Android 中出现内存泄漏的原因有哪些,如何发现和解决?

内存泄漏指的是程序在运行过程中,不断申请内存却没有及时释放不再使用的内存空间,导致可用内存逐渐减少。在 Android 中,内存泄漏的原因有多种。

一是持有不必要的静态引用。例如在 Activity 中定义了静态的 View 引用,当 Activity 被销毁时,由于静态引用的存在,该 View 及其关联的资源无法被释放。二是资源未正确关闭。比如在使用文件流、数据库连接等资源后,没有关闭它们,导致这些资源一直占用内存。三是内部类持有外部类的引用。当内部类的生命周期长于外部类时,可能导致外部类无法被释放。四是注册未注销的监听器。比如在 Activity 中注册了广播接收器,但在 Activity 销毁时没有注销,导致广播接收器及其相关的资源无法释放。

要发现内存泄漏,可以使用 Android Studio 自带的 Profiler 工具,它可以实时监测内存的使用情况,帮助定位内存泄漏的位置。还可以使用 LeakCanary 库,它能在内存泄漏发生时自动检测并给出详细的泄漏信息。

解决内存泄漏问题,首先要避免不必要的静态引用,对于需要在静态方法中使用的资源,尽量传递弱引用。对于资源未关闭的问题,要确保在使用完资源后及时关闭,如在 finally 块中关闭文件流等。对于内部类持有外部类引用的情况,可以将内部类改为静态内部类,并使用弱引用持有外部类。对于未注销的监听器,要在合适的时机,如 Activity 的 onDestroy 方法中注销监听器 。

解释 OOM(Out of Memory)的原因,当前应用可用内存为 20MB,已用 10MB,是否可能发生 OOM?

OOM 即内存溢出,是指程序在运行过程中申请的内存超过了系统所能提供的最大内存限制。

导致 OOM 的原因主要有以下几点。一是内存泄漏,如前面所述,不断申请内存却未及时释放,最终导致可用内存耗尽。二是加载了过大的资源,例如加载超高分辨率的图片,而没有进行适当的压缩处理,导致内存占用过大。三是频繁创建大量对象,且这些对象的生命周期较短,导致垃圾回收器频繁回收内存,但由于创建速度过快,最终可能导致内存不足。四是在一些低内存设备上,应用本身占用的内存已经接近系统分配给它的最大内存,此时再进行一些稍微大一点的内存操作,就可能引发 OOM。

即使当前应用可用内存为 20MB,已用 10MB,也有可能发生 OOM。比如在后续的操作中,需要加载一个大于 10MB 的资源,就可能导致 OOM。或者由于内存泄漏,虽然当前显示可用内存为 20MB,但实际上可分配的连续内存空间已经小于即将要申请的内存大小,也会引发 OOM。又或者频繁创建大量小对象,导致内存碎片化,虽然总的可用内存看起来足够,但无法分配出连续的大块内存来满足需求,同样会引发 OOM。

Android 中的跨进程通信方式有哪些,Binder 原理是什么,数据需要拷贝几次?

Android 中的跨进程通信方式主要有以下几种:

  • Bundle 传递数据:适用于在 Activity、Service 等组件间传递简单数据,通过 Intent 的 Bundle 来传递基本数据类型和可序列化的对象。
  • 文件共享:两个进程通过读写同一个文件来进行数据交换,但要注意并发访问的同步问题。
  • AIDL:用于实现不同进程间的接口调用,通过定义 AIDL 接口,服务端实现该接口,客户端调用接口方法来进行通信。
  • Messenger:基于消息的跨进程通信方式,通过 Message 和 Handler 来传递消息,适用于简单的消息传递场景。
  • ContentProvider:主要用于在不同应用之间共享数据,如共享联系人数据等,通过 ContentResolver 来访问 ContentProvider 提供的数据。

Binder 是 Android 中一种重要的跨进程通信机制。它的原理是,当客户端发起跨进程通信请求时,会通过系统底层的 Binder 驱动,将请求数据发送给服务端。Binder 驱动会为每个进程维护一个 Binder 引用表,通过这个表来找到目标服务端的 Binder 实体。服务端接收到请求后进行处理,并将结果通过 Binder 驱动返回给客户端。

关于数据拷贝次数,一般情况下,在客户端调用跨进程方法时,会将参数从客户端的用户空间拷贝到内核空间的 Binder 缓冲区,这是第一次拷贝。然后,Binder 驱动会将数据从内核空间的缓冲区拷贝到服务端的用户空间,这是第二次拷贝。当服务端返回结果时,同样需要进行两次拷贝,即从服务端的用户空间拷贝到内核空间,再从内核空间拷贝到客户端的用户空间。所以通常数据需要拷贝两次,但在一些特殊情况下,如使用了内存共享等技术,可能会减少拷贝次数。

使用 SharedPreferences 的 get 和 put 方法读写数据时可能面临的问题及 IO 性能方面的解决方法是什么?

当使用 SharedPreferences 的 get 和 put 方法时,可能会面临以下一些问题:

  • 性能问题:频繁地使用 put 方法写入数据和 get 方法读取数据,尤其是在主线程中进行这些操作时,可能会导致 UI 卡顿。因为 SharedPreferences 的读写操作实际上是对文件的读写,频繁操作会引起一定的 IO 开销。
  • 数据不一致问题:如果在多个地方同时对同一个 SharedPreferences 文件进行读写操作,可能会导致数据不一致。例如,一个线程正在写入数据,而另一个线程同时在读取数据,可能读取到的不是最新的完整数据。
  • 数据丢失问题:在某些极端情况下,如应用在写入 SharedPreferences 数据时突然崩溃,可能会导致数据丢失或损坏。

在 IO 性能方面,可以采取以下解决方法:

  • 异步操作:避免在主线程中直接使用 SharedPreferences 的读写方法。可以将读写操作放在异步线程中进行,例如使用线程池或者 AsyncTask 等方式,这样可以避免阻塞主线程,提高 UI 的响应性能。
  • 批量操作:如果需要多次写入或读取数据,尽量将这些操作合并为一次批量操作。比如,使用 SharedPreferences 的 edit 方法获取 Editor 对象后,多次调用 put 方法,最后再调用 commit 或 apply 方法提交修改。这样可以减少 IO 操作的次数,提高性能。
  • 缓存数据:对于频繁读取的数据,可以考虑在内存中进行缓存。在合适的时候,如应用启动时,将 SharedPreferences 中的数据读取到内存中的缓存变量中,后续读取数据时先从缓存中获取,只有在缓存中不存在或数据需要更新时,才从 SharedPreferences 中读取,这样可以减少对 SharedPreferences 的读取次数,提高性能。

一个应用如何发现当前局域网中其他也开启了该应用的设备?

要发现当前局域网中其他开启了该应用的设备,可以采用以下几种方法:

  • 基于 UDP 广播:应用可以在局域网内发送 UDP 广播消息,其他开启了该应用的设备接收到广播后进行响应。发送广播的设备可以监听特定端口,接收其他设备返回的响应消息,从而发现其他设备。例如,应用可以定义一个特定的广播消息格式,包含应用的标识等信息,其他设备接收到广播后,判断是否是同一应用的广播,如果是则返回自身的设备信息给发送广播的设备。
  • 使用多播技术:多播是一种介于单播和广播之间的通信方式,它可以将数据发送到一组特定的设备。应用可以加入一个多播组,在多播组内发送和接收消息,通过这种方式发现同一多播组内的其他应用设备。多播可以减少网络流量,因为消息只发送给加入了多播组的设备,而不是整个局域网。
  • 基于网络服务发现协议(如 Bonjour 或 mDNS):这些协议允许设备在局域网内自动发现和宣传网络服务。应用可以利用这些协议来注册和发现自身的服务,当其他设备开启了相同应用并注册了相同的服务时,就可以相互发现。例如,应用可以在启动时注册一个特定名称的服务,通过监听网络上的服务发现消息,发现其他注册了相同服务的设备。
  • 通过服务器中转:应用可以连接到一个公共的服务器,当设备开启应用时,向服务器发送上线消息,包含自身的设备信息和局域网 IP 等。其他设备可以从服务器获取当前在线的设备列表,从而发现局域网内的其他应用设备。这种方式需要有一个可靠的服务器来中转信息,但可以更方便地管理设备的发现和连接,尤其是在复杂的网络环境中。

Android 中 CRASH 和 ANR 的区别是什么,ANR 是如何发生的,系统如何发现 ANR?

CRASH 是指应用程序在运行过程中由于各种原因导致的崩溃,通常是由于程序出现了未处理的异常,如空指针异常、数组越界等,导致程序无法继续正常执行而突然退出。而 ANR 则是指应用程序在运行时无响应,这通常是因为应用程序在主线程中执行了耗时过长的操作,导致系统无法及时响应用户的输入事件等。

ANR 发生的原因主要有以下几种。一是在主线程中进行了复杂的计算或长时间的循环操作,阻塞了主线程。例如,在主线程中进行大规模的数据排序或加密解密等操作。二是进行网络请求或文件读取等耗时的 I/O 操作且未使用异步方式。比如在主线程中直接进行网络图片的下载。三是在主线程中等待某个锁或资源,导致主线程被挂起。

系统发现 ANR 主要是通过一个定时机制。Android 系统会在一定时间内检查主线程是否有响应,如果主线程在规定时间内没有处理完当前的任务,比如 5 秒内没有响应输入事件或 10 秒内没有执行完一个 BroadcastReceiver 的 onReceive 方法等,系统就会认为应用发生了 ANR,然后弹出 ANR 对话框提示用户,并在系统日志中记录相关信息,以便开发者进行分析和排查。

普通 for 循环和增强 for 循环的区别及使用场景是什么?

普通 for 循环和增强 for 循环有以下一些区别。首先,语法形式上,普通 for 循环需要明确指定循环的初始条件、循环终止条件和每次循环后的迭代操作。例如:for(int i = 0; i < array.length; i++) 。而增强 for 循环则更加简洁,它直接遍历数组或集合中的元素,不需要显式地获取索引等。例如:for(int num : array) 。

其次,功能特性方面,普通 for 循环可以方便地获取当前元素的索引,在需要根据索引进行一些复杂操作时非常有用。同时,它也可以灵活地控制循环的起始、终止和步长等。增强 for 循环则主要用于简单地遍历数组或集合,代码更加简洁易读,适用于不需要获取索引,只是单纯遍历元素的场景。

使用场景上,当需要对数组或集合中的元素进行修改,或者需要根据元素的位置进行一些特殊处理时,普通 for 循环更合适。比如对数组中的元素进行移位操作,或者在遍历二维数组时需要获取行列索引等。而增强 for 循环适合于快速遍历数组或集合,进行一些简单的读取操作,如遍历列表输出每个元素的值等,能使代码更加清晰简洁,减少了一些不必要的索引操作代码。

常用的排序算法及其时间、空间复杂度如何?

常见的排序算法有多种,以下是一些主要的排序算法及其复杂度分析。

  • 冒泡排序:它的基本思想是通过相邻元素的比较和交换,将最大(或最小)的元素逐步 “冒泡” 到数组的一端。时间复杂度方面,最好情况是数组已经有序,时间复杂度为 O (n),即只需要遍历一遍数组即可。最坏情况是数组完全逆序,时间复杂度为 O (n²),因为需要进行多次遍历和交换操作。平均时间复杂度也是 O (n²) 。空间复杂度为 O (1),因为只需要几个临时变量来进行交换操作,不需要额外开辟大量的空间。
  • 插入排序:它是将未排序的数据插入到已排序序列的合适位置。在最好情况下,数组已经有序,时间复杂度为 O (n),只需要依次比较插入即可。最坏情况是数组完全逆序,时间复杂度为 O (n²),每次插入都需要移动大量元素。平均时间复杂度同样是 O (n²)。空间复杂度为 O (1),它只需要一个额外的空间来存储当前要插入的元素。
  • 选择排序:每次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。它的时间复杂度无论最好还是最坏情况,均为 O (n²),因为它需要不断地在未排序部分寻找最小(或最大)元素。空间复杂度也是 O (1)。
  • 快速排序:通过选择一个基准元素,将数组分为两部分,左边的元素都小于基准,右边的元素都大于基准,然后递归地对左右两部分进行排序。快速排序的平均时间复杂度为 O (nlogn),但在最坏情况下,即数组已经有序或接近有序时,时间复杂度会退化为 O (n²)。空间复杂度在平均情况下为 O (logn),这是由于递归调用栈的深度,最坏情况下为 O (n)。

  • 归并排序:它采用分治法,将数组不断分成两半,分别排序后再合并。归并排序的时间复杂度始终为 O (nlogn),无论数据的初始状态如何。空间复杂度为 O (n),因为在合并过程中需要额外的数组来存储临时数据。

图片压缩的方法有哪些?

图片压缩主要有以下几种方法。

一是采样率压缩。这是通过降低图片的采样率来减少图片的像素数量,从而达到压缩图片大小的目的。例如,可以将图片的宽和高都缩小一定比例。在 Android 中,可以使用 BitmapFactory.Options 类来设置采样率。通过设置 inSampleSize 参数,大于 1 的值表示对图片进行采样压缩,比如设置为 2,则宽和高都变为原来的二分之一,像素数量变为原来的四分之一,图片文件大小也会相应大幅减小。这种方法适用于对图片清晰度要求不是特别高,主要用于快速显示预览图等场景。

二是质量压缩。它是在保持图片像素数量不变的情况下,通过改变图片的存储质量来压缩图片大小。Android 中可以使用 Bitmap.compress 方法,通过指定压缩格式如 JPEG、PNG 等,以及压缩质量参数来进行压缩。压缩质量参数取值范围一般是 0 到 100,数值越小,压缩后的图片质量越低,文件大小也越小。这种方法常用于需要在一定程度上保留图片清晰度,但又要减小文件大小的情况,比如上传图片到服务器时,在可接受的清晰度损失范围内尽量减小文件大小以节省网络流量和服务器存储空间。

三是使用第三方图片压缩库。例如,有一些开源的图片压缩库如 Luban 等,这些库通常结合了多种压缩算法和优化策略,可以更智能地根据图片的特点进行压缩。它们可以在保证一定图片质量的前提下,实现较高的压缩比。一些库还支持对图片进行裁剪、旋转等预处理操作后再进行压缩,能更好地满足各种应用场景的需求。使用第三方库可以节省开发时间,提高压缩效果和效率,尤其适用于处理大量图片或对图片压缩效果有较高要求的应用。

图片缓存如何实现,LruCache 算法的原理是什么?

图片缓存的实现主要有以下几个方面。

首先,需要有一个缓存容器来存储图片。可以使用一些集合类如 HashMap 等来存储图片的引用和对应的键值,以便快速查找和获取图片。然后,在加载图片时,先从缓存中查找是否已经存在该图片,如果存在则直接从缓存中取出使用,避免重复从网络或本地文件中加载,提高图片的加载速度。当图片从网络下载或从本地读取后,将其存入缓存中。

同时,为了防止缓存无限增长导致内存占用过多,需要有一定的缓存策略来管理缓存。这就涉及到缓存的淘汰机制,当缓存达到一定容量时,需要删除一些不常用的图片以腾出空间。

LruCache 算法的原理是基于最近最少使用原则。它认为最近使用过的图片在未来短期内更有可能被再次使用,而长时间未使用的图片则可以被淘汰。在 LruCache 中,当向缓存中添加新的图片时,如果缓存已满,它会淘汰掉最近最少使用的图片。具体实现上,它内部使用了一个 LinkedHashMap 来存储图片的键值对,LinkedHashMap 有一个特殊的构造函数,可以指定按照访问顺序来排列元素。当访问一个图片时,该图片对应的键值对会被移到链表的末尾,表示它是最近使用过的。当需要淘汰图片时,就从链表的头部删除元素,因为头部的元素是最近最少使用的。通过这种方式,LruCache 可以有效地管理图片缓存,既能提高图片的加载效率,又能避免内存的过度占用 。

HashMap 的原理和实现方式是什么?

HashMap 是 Java 中常用的一种数据结构,用于存储键值对。它基于哈希表的原理实现,通过对键进行哈希运算来确定存储位置,从而实现快速的查找、插入和删除操作 。

其内部主要由数组和链表(在 Java 8 之后,当链表长度达到一定阈值时会转化为红黑树)组成。当我们向 HashMap 中添加一个键值对时,首先会对键进行哈希运算,得到一个哈希值,然后通过这个哈希值与数组长度取模运算,得到在数组中的存储位置索引。如果该位置没有元素,就直接将键值对存储在该位置;如果该位置已经有元素了,就会以链表的形式将新的键值对挂载到该位置的链表上。在查找元素时,也是先通过哈希运算得到索引,然后遍历该索引位置的链表或红黑树来查找对应的键值对。

在实现方面,Java 中的 HashMap 类包含了多个重要的方法。比如 put 方法用于添加键值对,它会先计算键的哈希值,然后根据哈希值找到对应的存储位置,再将键值对插入。get 方法用于获取指定键的值,它同样先计算哈希值找到位置,然后在该位置的链表或红黑树中查找键并返回对应的值。此外,还有一些用于遍历、删除等操作的方法,共同构成了 HashMap 完整的功能体系。HashMap 的这种实现方式使得它在大多数情况下能够以接近常数时间的复杂度进行数据操作,提供了高效的数据存储和访问能力,但也需要注意哈希冲突等可能带来的性能问题 。

JVM 的基本工作原理和常用配置有哪些?

基本工作原理

JVM 即 Java 虚拟机,它是 Java 程序的运行核心。首先,Java 源代码会被编译成字节码文件,这些字节码文件是一种中间形式,不依赖于具体的硬件平台。当运行 Java 程序时,JVM 会负责加载字节码文件,并将字节码解释或编译成机器码在底层操作系统上执行。

JVM 主要包含了多个组件,如类加载器,它负责将字节码文件加载到内存中,形成类的元数据信息,包括类的结构、字段、方法等。运行时数据区则是程序运行时数据存储的地方,它又分为多个区域,比如堆区用于存储对象实例,栈区用于存储方法调用的栈帧,方法区用于存储类的信息、常量池等。执行引擎负责执行字节码指令,它可以采用解释执行和即时编译等不同的执行方式。在解释执行时,执行引擎会一条一条地读取字节码指令并执行;而在即时编译时,执行引擎会将热点代码编译成机器码,以提高执行效率。

常用配置

  • 堆内存配置:可以通过参数 “-Xmx” 和 “-Xms” 来设置堆的最大内存和初始内存大小。合理设置堆内存大小对于程序的性能至关重要,如果设置过小,可能会导致频繁的垃圾回收,影响程序性能;如果设置过大,可能会导致系统资源浪费。
  • 栈内存配置:通过 “-Xss” 参数可以设置每个线程的栈内存大小。栈内存主要用于存储方法调用的局部变量和栈帧等信息,如果栈内存设置过小,可能会导致栈溢出错误。
  • 垃圾回收器配置:可以选择不同的垃圾回收器,并对其进行相关参数配置。例如,使用 “-XX:+UseSerialGC” 可以指定使用串行垃圾回收器,它适合于单 CPU、小内存的环境;而 “-XX:+UseParallelGC” 则指定使用并行垃圾回收器,能利用多 CPU 的优势提高垃圾回收效率。还有其他更高级的垃圾回收器如 CMS 和 G1 等,也都有各自的配置参数来优化其性能。
  • 类加载相关配置:可以通过 “-verbose:class” 参数来查看类加载的详细信息,帮助排查类加载相关的问题。另外,还可以设置类加载的路径等,以确保程序能正确加载所需的类。

String 类型数据在 JVM 中的存储区域是哪里?

在 JVM 中,String 类型数据的存储位置较为复杂,主要与字符串的创建方式和内容有关。

当我们通过字面量形式创建一个字符串时,例如 String str = "Hello"; ,这个字符串会先在 JVM 的方法区中的常量池查找是否已经存在相同内容的字符串。如果常量池中已经存在,那么直接返回该字符串的引用;如果不存在,则会在常量池中创建一个新的字符串对象,并返回其引用。常量池是方法区的一部分,它主要用于存储编译期生成的各种字面量和符号引用。

而当我们通过 new 关键字创建一个字符串对象时,如 String str = new String ("Hello"); ,首先会在堆内存中创建一个新的字符串对象,这个对象的内容会指向方法区常量池中对应的字符串字面量。也就是说,此时在堆内存和方法区中都有与该字符串相关的存储。如果之后又有新的字符串通过字面量创建且内容与之前相同,仍然会使用常量池中的同一个字符串对象,但是通过 new 创建的字符串对象在堆内存中是不同的实例。

这种存储方式的设计有其优点。将字符串字面量存储在常量池中可以节省内存空间,因为相同内容的字符串只需要存储一份。同时,也便于快速地查找和比较字符串,提高了字符串操作的效率。但在实际编程中,也需要注意由于字符串存储的复杂性可能导致的一些问题,比如当对字符串进行修改操作时,可能会因为字符串的不可变性而产生一些意想不到的结果,以及可能会导致内存泄漏等问题,需要开发者对字符串的存储机制有清晰的认识才能更好地进行代码开发和优化。

MySQL 的相关知识点有哪些,包括基本操作、优化策略等?

基本操作

  • 数据库创建与删除:使用 “CREATE DATABASE” 语句可以创建一个新的数据库,例如 “CREATE DATABASE mydb;”,其中 “mydb” 是要创建的数据库名称。而使用 “DROP DATABASE” 语句可以删除一个数据库,如 “DROP DATABASE mydb;”,需要注意的是删除数据库会将其中的所有数据和表一并删除,操作需谨慎。
  • 表的创建、修改与删除:通过 “CREATE TABLE” 语句创建表,例如 “CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR (255), age INT);” 创建了一个名为 “users” 的表,包含 “id”“name” 和 “age” 三个列。“ALTER TABLE” 语句用于修改表的结构,如添加列 “ALTER TABLE users ADD COLUMN email VARCHAR (255);”。“DROP TABLE” 语句用于删除表,如 “DROP TABLE users;”。
  • 数据的插入、查询、更新和删除:使用 “INSERT INTO” 语句插入数据,如 “INSERT INTO users (id, name, age) VALUES (1, 'John', 25);”。“SELECT” 语句用于查询数据,例如 “SELECT * FROM users WHERE age > 20;” 可以查询出年龄大于 20 岁的所有用户信息。“UPDATE” 语句用于更新数据,如 “UPDATE users SET age = 26 WHERE id = 1;”。“DELETE FROM” 语句用于删除数据,如 “DELETE FROM users WHERE id = 1;”。

优化策略

  • 索引优化:合理创建索引可以大大提高查询速度。索引就像是一本书的目录,能快速定位到需要的数据。例如,在经常用于查询条件的列上创建索引,如 “CREATE INDEX idx_name ON users (name);” 在 “users” 表的 “name” 列上创建了索引。但索引也不是越多越好,过多的索引会导致插入、更新和删除操作变慢,因为每次操作都需要更新索引。
  • 查询语句优化:避免使用 “SELECT *”,尽量只查询需要的列,这样可以减少数据传输量。同时,合理使用连接查询,避免子查询的嵌套过深。例如,使用内连接 “SELECT u.name, o.order_id FROM users u INNER JOIN orders o ON u.id = o.user_id;” 比多层嵌套的子查询效率更高。
  • 数据库设计优化:设计良好的数据库结构对于性能至关重要。遵循范式原则,减少数据冗余,但也不能过度追求范式,有时适当的数据冗余可以提高查询效率。例如,在一些多对多关系中,可以增加中间表来存储关联信息。
  • 配置优化:合理配置 MySQL 的参数也能提升性能。比如调整缓存大小,“innodb_buffer_pool_size” 参数用于设置 InnoDB 存储引擎的缓冲池大小,增大这个值可以减少磁盘 I/O,提高数据库的读写性能。

Looper 是一个死循环,如何保证其性能?

Looper 在 Android 中主要用于处理消息循环,它确实是一个死循环,但有多种方式来保证其性能。

首先,Looper 在循环中主要是不断地从消息队列中取出消息并进行处理。在这个过程中,当消息队列中没有消息时,它会进入阻塞等待状态,而不是一直空转消耗 CPU 资源。这是通过 Linux 的管道机制和 epoll 等 I/O 多路复用技术实现的。当有新消息进入消息队列时,底层的机制会唤醒 Looper,使其继续从队列中取出消息进行处理,这样就避免了在无消息时的无效循环,从而提高了性能。

其次,对于消息的处理,Android 系统对消息进行了优先级的划分。重要的消息可以设置较高的优先级,使其能够优先被处理。例如,用户交互产生的消息通常会被赋予较高优先级,以保证界面的响应及时,而一些后台任务的消息则可以设置较低优先级,这样可以在系统资源紧张时,优先处理重要消息,合理分配系统资源,提高整体性能。

另外,在代码层面,开发者应该尽量避免在消息处理中进行复杂的、耗时的操作。如果确实有耗时操作,应该将其放在单独的线程中去执行,避免阻塞消息循环。例如,对于一些网络请求或者文件读写等耗时任务,应该通过异步线程来处理,当任务完成后再通过 Handler 发送消息通知主线程更新 UI 等,这样可以保证 Looper 的消息循环能够快速地处理其他消息,维持系统的流畅性。同时,合理地管理消息队列的长度也很重要,避免消息队列无限增长导致内存占用过大等问题,影响系统性能 。

Handler 如何延迟发送消息,其原理是什么?

Handler 主要用于在 Android 中进行线程间的通信,它延迟发送消息主要是通过调用 sendMessageDelayed() 或 postDelayed() 方法来实现的 。

当调用这些延迟发送消息的方法时,Handler 并不是立即将消息发送出去,而是将消息插入到消息队列 MessageQueue 中,并根据延迟时间计算出消息应该被处理的时间点。MessageQueue 内部是通过一个优先级队列来存储消息的,以确保消息按照时间顺序被取出和处理。

Handler 内部会关联一个 Looper,Looper 会不断地从 MessageQueue 中取出消息进行处理。当 Looper 在循环取消息时,会检查每条消息的执行时间,如果当前时间小于消息的执行时间,就会阻塞等待,直到时间到达消息的执行时间点才会取出该消息并交给对应的 Handler 进行处理。

例如,在一些场景中,我们需要在一段时间后执行某个任务,如延迟显示一个提示框或者定时更新 UI 等,就可以使用 Handler 的延迟发送消息功能。这样可以避免使用传统的 Thread.sleep() 等方法造成的线程阻塞问题,使得程序能够更加流畅地运行,合理地利用系统资源,提高应用的性能和响应性 。

线程池的工作原理和使用场景是什么?

线程池的工作原理主要基于对线程的复用和任务队列的管理。

首先,线程池在初始化时会创建一定数量的线程,这些线程处于空闲状态并等待任务的到来。当有新的任务提交到线程池时,线程池会根据当前线程的状态和任务队列的情况来决定如何处理该任务。如果有空闲线程,就会直接将任务分配给空闲线程去执行;如果没有空闲线程但任务队列未满,就会将任务放入任务队列中等待空闲线程来处理;而当任务队列已满且线程数量也达到了线程池的最大限制时,线程池会根据其饱和策略来决定是拒绝任务还是采取其他处理方式,比如等待一段时间看是否有线程空闲等。

线程池的使用场景非常广泛。在 Android 开发中,当需要频繁地执行一些耗时较短且任务数量较多的操作时,使用线程池可以避免频繁地创建和销毁线程所带来的性能开销。例如,在网络请求、图片加载等场景中,多个任务可以被提交到线程池中并发执行,提高任务执行效率,减少响应时间。同时,线程池也可以对线程的数量进行有效的控制,防止因线程过多导致系统资源耗尽,从而保证应用的稳定性和性能。

当需要加载一个类时,在当前 ClassLoader 中没有找到该类会怎样(双亲委派),如果最后也没找到会抛出什么异常(ClassNotFound),抛出异常后会怎样处理?

在 Java 的类加载机制中,采用了双亲委派模型。当需要加载一个类时,首先会由当前的类加载器(ClassLoader)尝试去加载,如果在当前类加载器中没有找到该类,它就会委托给它的父类加载器去加载。这个过程会一直向上传递,直到到达最顶层的启动类加载器。只有当父类加载器都无法加载该类时,才会由当前类加载器自己尝试去加载其他来源的类,比如从网络加载或者从自定义的路径加载等。

如果最后所有的类加载器都没有找到要加载的类,就会抛出 ClassNotFoundException 异常。当抛出这个异常后,在不同的场景下会有不同的处理方式。在一般的 Java 程序中,如果是在程序的关键路径上,比如在初始化一个重要的对象或者执行一个关键业务逻辑时抛出该异常,可能会导致程序的后续逻辑无法正常执行,甚至可能导致程序崩溃。但如果是在一些非关键的、具有容错性的场景中,比如在加载一些可选的插件或者动态加载某些功能模块时,程序可以选择捕获这个异常并进行相应的处理,比如给用户提示加载失败的信息,或者尝试采取其他的补救措施,如从其他地方重新获取该类或者使用默认的替代类等。

解释 AtomicReference 和 compareAndSet 的作用和应用场景。

AtomicReference 是 Java 中的一个原子引用类型,它提供了一种对对象引用进行原子操作的方式。原子操作意味着这些操作在多线程环境下是线程安全的,不会被其他线程中断或干扰。

compareAndSet 是 AtomicReference 提供的一个重要方法,它的作用是比较当前的引用值与预期值是否相等,如果相等,则将引用值更新为新的值,这个操作是原子性的。也就是说,在多线程环境下,多个线程同时调用 compareAndSet 方法时,只有一个线程能够成功地更新引用值,其他线程的操作会失败,但不会导致数据的不一致性。

其应用场景主要在多线程并发访问共享资源且需要保证数据一致性的情况下。例如,在一些并发编程中,多个线程可能会同时访问和修改一个共享的对象引用。使用 AtomicReference 和 compareAndSet 可以确保在并发修改时,对象引用的状态始终是正确的和一致的。比如在实现一个简单的缓存系统时,多个线程可能会同时尝试更新缓存中的某个对象引用,通过使用 AtomicReference 的 compareAndSet 方法,可以安全地实现对缓存对象的更新,避免出现数据不一致的问题。又如在一些并发的数据结构实现中,也经常会用到 AtomicReference 来保证节点引用等的原子性操作,从而确保整个数据结构在多线程环境下的正确性和稳定性 。

最近更新:: 2025/10/23 21:22
Contributors: luokaiwen