问题链接:科大讯飞安卓客户端面经_牛客网 作者:摸不着头发
1. 说一下项目中的难点(说了并发场景)
2. 除了加锁,有没有性能更好的方法?
Android 高性能并发控制方案(替代传统锁机制)
针对多线程场景下的性能优化,除传统锁机制外,可结合以下策略实现低竞争、高吞吐量的并发处理:
1. 无锁数据结构与原子操作
- Atomic 原子类:
使用AtomicInteger
、AtomicReference
等原子类,通过 CAS(Compare-and-Swap)机制实现线程安全操作,避免锁竞争开销。例如计数器场景:AtomicInteger counter = new AtomicInteger(0); counter.incrementAndGet(); // 原子递增,无需锁:ml-citation{ref="2,8" data="citationList"}
- 并发集合:
优先选用ConcurrentHashMap
、CopyOnWriteArrayList
等线程安全容器,其内部通过分段锁或无锁设计优化并发性能。
2. 异步化与非阻塞编程
- SharedPreferences 异步提交:
使用apply()
替代commit()
方法,异步写入数据至磁盘,避免主线程阻塞。editor.putString("key", "value").apply(); // 异步提交,无锁等待:ml-citation{ref="2" data="citationList"}
- 协程与 Flow:
通过 Kotlin 协程的Mutex.withLock
非阻塞挂起机制,减少线程切换开销。例如:val mutex = Mutex() coroutineScope { mutex.withLock { // 非阻塞式挂起而非线程阻塞 // 临界区操作:ml-citation{ref="8" data="citationList"} } }
3. 资源池化与单例模式
- 数据库连接池:
使用SQLiteOpenHelper
结合对象池(如 Guava 的Suppliers.memoize
),复用数据库连接实例,降低创建开销和资源争用。Supplier<SQLiteOpenHelper> dbSupplier = Suppliers.memoize(() -> new MyDbHelper());
- 单例缓存:
高频访问资源(如网络请求客户端)采用双重检查锁定(DCL)实现线程安全单例:private volatile static ApiClient instance; public static ApiClient getInstance() { if (instance == null) { synchronized (ApiClient.class) { if (instance == null) { instance = new ApiClient(); } } } return instance; // 减少锁范围:ml-citation{ref="8" data="citationList"} }
4. 批处理与事务优化
- SQLite 批量事务:
合并多次数据库操作为单次事务,减少磁盘 I/O 和锁持有时间:db.beginTransaction(); try { for (Data item : dataList) { insertData(db, item); // 批量插入 } db.setTransactionSuccessful(); } finally { db.endTransaction(); // 事务提交后自动释放锁:ml-citation{ref="5,8" data="citationList"} }
- 内存缓冲队列:
使用BlockingQueue
实现生产者-消费者模式,通过缓冲层解耦并发压力,例如日志异步写入。
性能对比与实践建议
方案 | 适用场景 | 性能提升点 |
---|---|---|
原子类与并发集合 | 计数器、状态标记 | 减少锁竞争与上下文切换 |
异步提交与协程 | UI 线程敏感操作 | 避免主线程阻塞 |
资源池化与单例 | 数据库连接、网络客户端 | 降低资源创建开销 |
批处理与事务 | 大数据量写入/更新 | 减少 I/O 和锁持有时间 |
最佳实践:
- 优先无锁设计:在非强一致性要求的场景(如统计计数)使用原子类。
- 控制锁粒度:缩小同步代码块范围,避免在锁内执行耗时操作。
- 异步化改造:对磁盘/网络等慢操作采用异步队列处理。
通过上述策略,可在保障线程安全的前提下显著提升并发性能,尤其适用于高频率、低延迟要求的场景。
3. 为什么不推荐直接new一个线程?
为何不推荐直接 new
线程?
在开发中,直接通过 new Thread()
创建线程的方式存在以下问题:
-
性能开销大
每次创建新线程都会涉及内存分配、上下文切换等底层开销,频繁创建和销毁线程会导致性能显著下降。线程池通过复用已有线程,减少此类开销。 -
资源耗尽风险
直接创建线程时,线程数量不受控,可能导致内存溢出(OutOfMemoryError
)或 CPU 过载。线程池通过限制最大线程数(maximumPoolSize
)和任务队列(workQueue
),防止资源被耗尽。 -
管理困难
直接创建的线程生命周期不可控,难以统一调度、监控和复用。线程池提供任务队列、超时机制、拒绝策略等功能,可优化任务执行顺序和资源分配。 -
内存泄漏风险
在 Android 等场景中,直接创建线程可能导致内存泄漏。例如,若线程持有Activity
的引用且未及时释放,会阻止垃圾回收,最终引发内存泄漏。 -
系统稳定性问题
线程池通过核心参数(如corePoolSize
、keepAliveTime
)和拒绝策略(如丢弃任务或抛出异常),避免因线程过多导致系统崩溃。直接创建线程则无法实现此类容错机制。
线程池的优势
通过自定义 ThreadPoolExecutor
,可灵活配置以下参数以优化性能:
- 核心线程数(
corePoolSize
):长期存活的线程数量。 - 最大线程数(
maximumPoolSize
):控制并发峰值。 - 存活时间(
keepAliveTime
):回收闲置线程以节省资源。 - 任务队列(
workQueue
):缓冲任务,防止瞬时高负载。
总结
直接 new
线程适用于极简场景,但在高并发或资源敏感型系统中,线程池能显著提升性能、稳定性和可维护性。通过合理配置线程池参数,可避免资源浪费和系统崩溃风险。
4. Sleep和wait的区别?
sleep()
与 wait()
的核心区别
1. 语法与所属类
-
sleep()
是Thread
类的静态方法,可直接调用(如Thread.sleep(1000)
),无需依赖同步锁即可使用。 -
wait()
是Object
类的实例方法,必须在synchronized
代码块或方法中调用,否则会抛出IllegalMonitorStateException
。
2. 锁的释放行为
-
sleep()
线程进入阻塞状态(TIMED_WAITING
),不会释放已持有的锁,其他线程无法获取该锁资源。 -
wait()
线程主动释放当前持有的锁,进入等待队列(WAITING
或TIMED_WAITING
),允许其他线程竞争锁资源。
3. 唤醒机制
-
sleep()
通过指定休眠时间(如sleep(1000)
)自动唤醒,或通过interrupt()
强制中断休眠。 -
wait()
必须依赖其他线程调用同一对象的notify()
或notifyAll()
来唤醒。若未指定超时时间,会永久等待直到被唤醒。
4. 设计目的与使用场景
-
sleep()
用于暂停当前线程的执行,常见于模拟耗时操作、控制执行节奏或让出CPU资源。 -
wait()
用于线程间通信与同步,通常与notify()
配合实现条件等待(如生产者-消费者模型)。
5. 线程状态变化
-
sleep()
线程状态从RUNNABLE
转为TIMED_WAITING
(带超时)或WAITING
(无超时)。 -
wait()
线程状态转为WAITING
(无超时)或TIMED_WAITING
(带超时),并释放锁资源。
总结对比表
特性 | sleep() | wait() |
---|---|---|
所属类 | Thread | Object |
锁释放 | ❌ 不释放 | ✅ 释放 |
唤醒方式 | 自动唤醒或中断 | 依赖 notify() /notifyAll() |
同步依赖 | 无需同步锁 | 必须在 synchronized 块内 |
典型场景 | 线程暂停、时间控制 | 线程协作、条件等待 |
通过合理选择 sleep()
或 wait()
,可优化多线程程序的资源利用率和执行效率。
5. Looper死循环为什么不会卡死?
Looper死循环不卡死主线程的核心原理
1. 消息循环机制的本质
主线程的Looper.loop()
是一个事件驱动的消息循环,通过MessageQueue
持续处理消息(如点击事件、生命周期回调等)。其设计目标是异步处理任务而非空转,当队列为空时,会通过底层epoll
机制进入休眠状态,不占用CPU资源。
2. 阻塞与唤醒原理
- 阻塞时机:当
MessageQueue
为空时,queue.next()
方法通过nativePollOnce()
进入阻塞状态(类似Linux的epoll
机制),此时主线程释放CPU资源。 - 唤醒机制:当新消息(如用户操作、系统事件)通过
Handler
发送到队列时,enqueueMessage()
方法触发nativeWake()
唤醒主线程,继续处理消息。
3. 任务调度方式
- 主线程任务以消息形式存在:所有操作(如Activity生命周期方法
onCreate
、UI更新等)均封装为Message
,由主线程的Looper
按顺序处理。 - 按优先级处理消息:消息队列根据
when
字段(时间戳)排序,优先处理定时任务和同步屏障后的异步消息(如UI绘制)。
4. 资源消耗控制
- 零消息时无消耗:主线程在无消息时处于休眠状态,不会持续执行循环逻辑,因此不会导致CPU过载或卡死。
- 高效事件分发:消息处理过程中,每次仅执行一个
Message
任务,完成后立即返回循环,避免长时间占用主线程。
总结对比
关键机制 | 作用与效果 |
---|---|
消息驱动 | 所有任务封装为消息,按顺序执行,避免无序抢占资源。 |
阻塞休眠 | 队列空时释放CPU,减少资源消耗。 |
异步唤醒 | 新消息到达时精准唤醒线程,确保响应及时性。 |
通过以上机制,Android主线程既能保持消息处理的高效性,又避免了传统死循环导致的应用卡死问题。
6. 线程之间如何进行通信?
参考另一篇文章:《Android面试高频问题》第5条
7. rxjava和协程解决了什么问题?
RxJava与协程解决的问题对比
一、RxJava的核心解决场景
-
异步回调地狱
RxJava通过链式调用和操作符(如flatMap
、zip
)将嵌套的异步操作转换为线性流程,典型场景如多级网络请求的合并处理。
示例:合并上传头像和更新用户信息的多个异步任务。 -
复杂数据流处理
支持动态过滤、合并或转换数据流,例如:- 实时搜索输入防抖:
debounce
操作符延迟触发请求,避免高频输入导致的冗余请求。 - 分页加载:
switchMap
取消前序未完成的请求,确保只处理最新分页数据。
- 实时搜索输入防抖:
-
线程管理难题
subscribeOn
和observeOn
可灵活切换线程,避免手动管理线程池。- 适用于后台计算与主线程UI更新的场景(如网络请求+UI渲染)。
-
背压问题
通过Flowable
和背压策略(如BUFFER
、DROP
)处理生产速度超过消费速度的场景(如传感器高频数据流)。
二、协程的核心解决场景
-
简化异步代码
- 通过挂起函数(
suspend
)将异步代码写成同步形式,消除回调嵌套。 - 示例:协程中直接顺序执行网络请求和UI更新,无需回调接口。
- 通过挂起函数(
-
结构化并发
- 使用协程作用域(如
viewModelScope
)管理生命周期,避免内存泄漏(如Activity
销毁时自动取消任务)。 - 示例:取消后台任务时,协程自动释放资源,无需手动标记
isCancelled
。
- 使用协程作用域(如
-
轻量级线程切换
- 通过
Dispatchers
(如IO
、Main
)切换线程,代码更简洁(对比RxJava的Schedulers.io()
)。 - 示例:协程中只需指定
withContext(Dispatchers.IO)
即可切换至IO线程。
- 通过
-
减少模板代码
协程通过launch
和async
替代传统回调接口,减少冗余代码量。
三、适用场景对比
场景 | RxJava优势 | 协程优势 |
---|---|---|
复杂数据流处理 | 丰富的操作符(debounce 、switchMap ) | 代码直观性高,适合简单异步逻辑 |
线程切换灵活性 | 需显式指定调度器(如Schedulers.io() ) | 通过Dispatchers 简化线程切换 |
内存泄漏风险控制 | 需手动取消订阅(CompositeDisposable ) | 结构化并发自动管理生命周期 |
学习曲线 | 高(需掌握操作符和响应式编程范式) | 低(类似同步代码的写法) |
总结
- RxJava更适合需要复杂数据流处理(如实时搜索、多任务合并)的场景。
- 协程在简化异步逻辑、减少内存泄漏风险和代码可读性方面表现更优,尤其适合网络请求、数据库操作等常见异步任务。
- 随着Kotlin协程的普及,其在Android开发中逐渐成为处理异步逻辑的首选,但RxJava在特定场景(如背压控制)仍不可替代。
8. mvvm与mvp的区别,有什么好处?
MVVM与MVP的核心区别及优势对比
一、核心架构差异
维度 | MVP | MVVM |
---|---|---|
核心角色 | Model-View-Presenter | Model-View-ViewModel |
数据流向 | View ↔ Presenter ↔ Model(双向通信) | View ↔ ViewModel ↔ Model(单向绑定) |
通信方式 | 接口回调(View与Presenter定义接口) | 数据绑定(如LiveData、DataBinding) |
依赖关系 | View依赖Presenter,Presenter依赖Model | View与ViewModel通过观察者模式解耦 |
二、MVP的优缺点
-
优势
- 职责清晰:Presenter处理业务逻辑,View仅负责UI,适合复杂业务场景(如多步骤表单校验)。
- 强控制性:通过接口明确View与Presenter的交互,便于调试(如Mock测试)。
- 兼容性强:不依赖特定框架,适合传统项目或混合开发(如WebView与Native混合)。
-
劣势
- 接口膨胀:每个View需定义大量接口,导致模板代码增加(如
LoginView
接口定义10+方法)。 - 手动同步:需主动调用接口方法更新UI,易遗漏状态同步(如网络请求后忘记调用
showSuccess()
)。
- 接口膨胀:每个View需定义大量接口,导致模板代码增加(如
三、MVVM的优缺点
-
优势
- 自动化UI更新:通过数据绑定(如
LiveData
)自动刷新UI,减少手动同步代码(代码量减少30%+)。 - 低耦合:ViewModel与View无直接依赖,便于复用(如同一ViewModel用于手机和平板界面)。
- 生命周期感知:Jetpack ViewModel自动管理数据,避免内存泄漏(如
Activity
重建时保留数据)。
- 自动化UI更新:通过数据绑定(如
-
劣势
- 学习成本高:需掌握数据绑定、观察者模式等概念(如误用
DataBinding
导致XML复杂度上升)。 - 调试困难:数据绑定错误可能隐藏在XML中,难以追踪(如绑定表达式空指针)。
- 学习成本高:需掌握数据绑定、观察者模式等概念(如误用
四、适用场景对比
场景 | 推荐模式 | 原因 |
---|---|---|
简单UI+高频数据更新 | MVVM | 数据绑定自动刷新UI(如实时股票行情展示) |
复杂业务逻辑 | MVP | 通过接口精准控制流程(如电商订单多状态管理) |
跨平台复用 | MVVM | ViewModel可共享至Compose、Flutter等框架(如统一逻辑层) |
遗留项目维护 | MVP | 无强制框架依赖,渐进式改造(如逐步替换MVC) |
总结:MVVM vs MVP的核心优势
-
MVVM:
- ✅ 减少模板代码:通过数据绑定简化UI更新。
- ✅ 生命周期安全:Jetpack组件天然支持Android生命周期。
- ✅ 更适合响应式UI:如实时搜索、动态列表更新。
-
MVP:
- ✅ 精细控制逻辑:接口明确,适合复杂交互场景。
- ✅ 框架无关性:适用于无数据绑定支持的项目(如旧版Java项目)。
选择建议
- 优先MVVM:新项目或需要快速迭代的UI驱动型应用(如社交、新闻类App)。
- 选择MVP:强业务逻辑或需高度定制化的场景(如金融、企业级后台系统)。
9. 什么是内存泄露,安卓中有哪些场景?
内存泄漏的定义
内存泄漏(Memory Leak) 指程序申请内存后,因未正确释放不再使用的内存,导致该内存长期被占用且无法回收的现象。这会逐步消耗可用内存,最终可能引发应用卡顿、崩溃或系统资源耗尽。
安卓中常见的内存泄漏场景
-
单例模式持有短生命周期对象的引用
- 问题:单例的生命周期与应用一致,若其持有
Activity
或Fragment
等短生命周期组件的引用,组件销毁后无法释放内存。 - 示例:单例类中直接使用
Activity Context
而非Application Context
。
- 问题:单例的生命周期与应用一致,若其持有
-
静态集合类长期持有对象
- 问题:
HashMap
、Vector
等静态集合类会持续引用其存储的对象,即使这些对象已不再需要。 - 示例:全局缓存未清理的
Bitmap
或自定义对象列表。
- 问题:
-
匿名内部类隐式持有外部类引用
- 问题:匿名内部类(如
Handler
、Runnable
)默认持有外部类(如Activity
)的引用,若未及时释放会导致外部类无法回收。 - 示例:延迟消息的
Handler
未移除,或异步任务未取消。
- 问题:匿名内部类(如
-
未关闭的资源引用
- 问题:数据库连接、文件流、
BroadcastReceiver
等资源未及时关闭或注销,导致关联内存无法释放。 - 示例:
Cursor
或InputStream
使用后未调用close()
。
- 问题:数据库连接、文件流、
-
监听器或回调未注销
- 问题:注册的系统服务(如传感器、定位)或自定义监听器未在组件销毁时取消注册,导致组件被隐式引用。
- 示例:
SensorManager
注册后未调用unregisterListener()
。
-
静态View或Drawable引用Activity
- 问题:静态变量引用了
View
或Drawable
,间接持有Activity
的Context
,阻碍Activity
销毁后的内存回收。
- 问题:静态变量引用了
-
线程或AsyncTask生命周期管理不当
- 问题:后台线程(如
AsyncTask
)持有Activity
引用,且未在组件销毁时终止任务,导致内存滞留。
- 问题:后台线程(如
解决方案
-
避免长生命周期对象持有短生命周期引用
- 使用
Application Context
替代Activity Context
,或通过弱引用(WeakReference
)间接持有对象。
- 使用
-
及时释放资源
- 手动关闭数据库、文件流,移除
Handler
中的消息,取消异步任务。
- 手动关闭数据库、文件流,移除
-
使用弱引用或软引用
- 对缓存或监听器使用
WeakHashMap
或WeakReference
,避免强引用导致对象无法回收。
- 对缓存或监听器使用
-
工具检测与分析
- 利用
Android Profiler
、LeakCanary
等工具追踪泄漏路径,定位未释放的引用链。
- 利用
总结:内存泄漏的核心是对象生命周期管理失衡,通过合理控制引用关系、及时释放资源及工具辅助检测,可有效减少泄漏风险。
10. 什么是内存抖动?
内存抖动的定义与影响
内存抖动(Memory Churn) 指在短时间内频繁创建和销毁大量临时对象,导致垃圾回收(GC)反复触发,占用主线程资源,引发应用卡顿甚至ANR(应用无响应)。
内存抖动的典型场景与原因
-
高频创建临时对象
- 场景:在
onDraw()
、onBindViewHolder()
等高频回调中重复创建对象(如Paint
、Bitmap
)。 - 示例:
fun onDraw() { val paint = Paint() // 每次绘制都新建Paint对象 canvas.drawText("text", 0, 0, paint) }
- 后果:单次绘制可能仅生成1个对象,但每秒60帧的刷新率会导致每秒60次对象创建和GC压力。
- 场景:在
-
循环中分配内存
- 场景:在循环体内反复创建集合或大对象(如解析JSON时生成临时字符串)。
- 示例:
fun parseData(list: List<String>) { list.forEach { item -> val tempList = ArrayList<String>() // 每次循环新建List tempList.add(item) } }
-
频繁触发GC
- 表现:通过Android Profiler观察内存曲线呈锯齿状,伴随频繁的GC事件(如
GC_ALLOC
日志)。
- 表现:通过Android Profiler观察内存曲线呈锯齿状,伴随频繁的GC事件(如
内存抖动的优化策略
-
对象复用与缓存
- 方法:将高频使用的对象提取为成员变量或静态变量,避免重复创建。
- 示例:
private val paint = Paint() // 复用Paint对象 fun onDraw() { canvas.drawText("text", 0, 0, paint) }
-
使用对象池(Object Pool)
- 适用场景:需要频繁创建同类对象时(如
Bitmap
、自定义模型)。 - 工具:Android的
Pools
类或第三方库(如RecyclerView
的ViewHolder池)。
- 适用场景:需要频繁创建同类对象时(如
-
优化数据结构
- 方法:优先选择轻量级数据结构(如
SparseArray
替代HashMap
),减少内存分配次数。
- 方法:优先选择轻量级数据结构(如
-
避免在关键路径中分配内存
- 原则:在
onDraw()
、onScroll()
等高频回调中禁止新建对象,预初始化所有资源。
- 原则:在
检测与分析工具
-
Android Profiler
- 功能:实时监控内存分配与GC事件,定位高频分配代码块。
- 操作:
- 启动Memory Profiler,记录内存分配。
- 筛选
Allocations
视图,按调用栈排序,找到分配量最大的类和方法。
-
Systrace/Perfetto
- 功能:分析GC导致的UI线程阻塞(如
GC Pause
事件占比过高)。
- 功能:分析GC导致的UI线程阻塞(如
内存抖动 vs 内存泄漏
维度 | 内存抖动 | 内存泄漏 |
---|---|---|
核心问题 | 频繁GC导致主线程卡顿 | 对象无法回收导致内存持续增长 |
内存曲线 | 锯齿状波动(快速分配与释放) | 持续上升(无释放) |
优化重点 | 减少临时对象分配 | 修复无效引用 |
总结
内存抖动的本质是高频内存分配与GC压力,通过对象复用、数据预加载及工具分析,可显著降低其对性能的影响。在性能敏感场景(如列表滚动、动画渲染)中,需严格避免临时对象分配。
11. 什么是过度绘制?
过度绘制(Overdraw)的定义
过度绘制指同一像素在同一帧(16.67ms)内被多次绘制的现象,通常由多层次UI叠加或不可见图层参与绘制导致,造成GPU和CPU资源浪费。例如,一个蓝色背景的按钮上叠加透明图标,底层背景可能被重复绘制多次。
检测方法与颜色标准
-
调试工具
- 在Android开发者选项中启用“调试GPU过度绘制”,屏幕会通过颜色标记不同区域的绘制次数。
- 颜色规则:
- 原色:无过度绘制(1次绘制)
- 蓝色:1次过度绘制(2次绘制)
- 绿色:2次过度绘制(3次绘制)
- 粉色:3次过度绘制(4次绘制)
- 红色:4次及以上过度绘制(5次及以上绘制)。
-
性能影响
- 丢帧与卡顿:单帧绘制时间超过16.67ms会导致丢帧,用户感知为界面卡顿。
- 资源浪费:不可见图层(如被遮挡的背景)参与绘制,增加GPU负载和耗电量。
优化策略
-
减少UI层级
- 合并或移除不必要的布局嵌套(如用
ConstraintLayout
替代LinearLayout
),降低视图树深度。
- 合并或移除不必要的布局嵌套(如用
-
避免透明与半透明元素
- 透明区域(如
alpha<1
的View)会强制GPU进行混合计算,优先使用不透明背景或setWillNotDraw(true)
跳过绘制。
- 透明区域(如
-
复用布局与视图
- 通过
<include>
标签复用公共布局,或使用ViewStub
延迟加载非必要视图。
- 通过
-
优化自定义View
- 在
onDraw()
中避免频繁创建对象(如Paint
、Path
),通过clipRect()
限制绘制区域。
- 在
总结
过度绘制的核心问题是无效像素重复渲染,通过层级优化、透明控制及工具检测,可显著降低绘制次数,提升应用流畅度与能效。
12. 在开发过程中什么时候会出现UI卡顿的现象?
开发过程中引发UI卡顿的典型场景
以下为开发中可能导致UI卡顿的关键场景及其原因,结合性能优化实践和工具分析总结:
1. 布局设计不合理
- 场景:
- 布局嵌套层级过深(如多层
LinearLayout
嵌套),导致measure
和layout
阶段耗时增加。 - 使用复杂
ViewGroup
(如RelativeLayout
未优化约束条件),计算视图位置时消耗过多CPU资源。
- 布局嵌套层级过深(如多层
2. 主线程执行耗时操作
- 场景:
- 在UI线程中执行文件读写、数据库查询、网络请求等I/O操作,阻塞界面渲染。
- 主线程中进行大量数据解析(如JSON/XML解析)或复杂计算(如图像处理),导致单帧时间超过16ms。
3. 动画或高频UI更新未优化
- 场景:
- 同时执行多个复杂动画(如
ValueAnimator
、ObjectAnimator
),未合理控制动画帧率或复用动画资源。 - 列表滚动时频繁调用
notifyDataSetChanged()
,触发冗余的布局重绘(如RecyclerView
未使用差分更新)。
- 同时执行多个复杂动画(如
4. 内存资源管理不当
- 场景:
- 频繁GC:短时间内创建大量临时对象(如
onDraw()
中频繁创建Paint
或Bitmap
),触发垃圾回收并暂停UI线程。 - 内存泄漏:
Activity
或Fragment
因静态引用未被释放,导致内存持续占用,最终引发卡顿甚至崩溃。
- 频繁GC:短时间内创建大量临时对象(如
5. 过度绘制(Overdraw)
- 场景:
- 叠加多个半透明图层(如背景和前景均设置透明色),导致同一像素被多次绘制,GPU负载过高。
- 自定义
View
未使用clipRect()
限制绘制区域,绘制超出屏幕范围的无效内容。
6. 数据绑定与渲染效率低下
- 场景:
- 列表项绑定数据时未使用
ViewHolder
复用机制,每次滚动均重新创建视图对象。 - 使用低效的数据结构(如嵌套
HashMap
)或未分页加载海量数据,导致UI线程处理时间激增。
- 列表项绑定数据时未使用
总结
UI卡顿的核心矛盾在于单帧渲染时间超过16ms,需重点关注布局优化、线程管理、内存控制及绘制效率。通过工具(如Android Profiler、Systrace)定位瓶颈,结合代码规范和性能优化策略(如异步处理、对象复用)可显著降低卡顿风险。
13. https与http的区别?
HTTPS 与 HTTP 的核心区别
HTTPS(HyperText Transfer Protocol Secure)是 HTTP 的安全版本,通过 SSL/TLS 加密协议 保障数据传输的机密性和完整性。以下是两者的主要区别:
1. 安全性
特性 | HTTP | HTTPS |
---|---|---|
数据加密 | 明文传输,易被窃听或篡改 | 所有数据加密传输(对称加密+非对称加密) |
身份验证 | 无服务器身份验证,易受中间人攻击 | 需 CA 颁发的 SSL 证书,验证服务器身份 |
数据完整性 | 无完整性校验,数据可能被篡改 | 通过哈希算法(如 SHA-256)校验数据完整性 |
2. 协议与端口
协议层 | HTTP | HTTPS |
---|---|---|
传输层协议 | 基于 TCP | 基于 TCP + SSL/TLS 加密层 |
默认端口 | 80 | 443 |
3. 性能影响
性能维度 | HTTP | HTTPS |
---|---|---|
连接速度 | 无加密开销,延迟低 | 因 TLS 握手和加密计算,首次连接延迟略高 |
资源消耗 | CPU/内存占用低 | 加密解密增加 CPU 开销(现代硬件影响较小) |
HTTP/2 支持 | 可支持,但浏览器普遍要求 HTTPS | 天然支持 HTTP/2,提升多路复用和头部压缩效率 |
4. 应用场景
场景 | HTTP | HTTPS |
---|---|---|
敏感数据传输 | ❌ 不适用(如登录、支付) | ✅ 必须使用(防数据泄露、防钓鱼) |
SEO 优化 | ❌ 搜索引擎对 HTTP 网站降权 | ✅ Google 等优先收录 HTTPS 站点 |
浏览器兼容性 | ❌ 现代浏览器标记为“不安全” | ✅ 地址栏显示锁形图标,增强用户信任 |
HTTPS 的工作原理(简化版)
-
TLS 握手:
- 客户端发送支持的加密套件列表。
- 服务器返回证书和选定的加密套件。
- 客户端验证证书合法性,生成对称密钥(用服务器公钥加密后传输)。
- 双方使用对称密钥加密后续通信。
-
数据传输:
- 使用对称加密(如 AES)加密数据,非对称加密(如 RSA)保护密钥交换。
HTTP 的典型风险
- 窃听攻击:明文传输的密码、Cookie 可被截获。
- 中间人攻击:攻击者伪造服务器或篡改数据(如广告注入)。
- 数据篡改:HTTP 响应可能被劫持并插入恶意代码。
总结
维度 | HTTP | HTTPS |
---|---|---|
核心价值 | 简单、快速 | 安全、可信 |
适用场景 | 内部网络、静态资源缓存 | 所有公开 Web 服务(现代标配) |
未来趋势 | 逐渐被淘汰 | 强制升级(如浏览器限制 HTTP) |
优先选择 HTTPS:现代 Web 开发中,HTTPS 已成为强制性标准,尤其涉及用户隐私或金融交易时。通过免费证书(如 Let's Encrypt)和 CDN 加速,部署成本已大幅降低。
14. 安卓端中如何获取ca证书,以及获取后是否要保存在客户端(不知道)
安卓端获取CA证书的方法
1. 手动获取与安装
- 从应用内导出:部分工具类应用(如HTTPCanary)支持直接导出CA证书,用户需进入应用设置→“SSL证书设置”→“导出证书”生成
.pem
或.cer
文件。 - 从服务器下载:若需信任特定服务器证书,可从服务器管理员处获取
.crt
或.pem
文件,或通过浏览器访问服务器地址手动下载证书链。
2. 代码动态获取
- 读取本地证书文件:将CA证书文件(如
server_certificate.pem
)放入assets
目录,通过AssetManager
读取并转换为X509Certificate
对象。InputStream inputStream = context.getAssets().open("server_certificate.pem"); CertificateFactory cf = CertificateFactory.getInstance("X.509"); X509Certificate caCert = (X509Certificate) cf.generateCertificate(inputStream);
- 系统API安装:使用
KeyChain
类触发系统级证书安装弹窗,用户确认后证书将存入系统信任库。Intent intent = KeyChain.createInstallIntent(); intent.putExtra(KeyChain.EXTRA_CERTIFICATE, caCertBytes); // caCertBytes为证书字节流 startActivity(intent);
3. 调试工具抓包获取
- 抓包工具证书:使用Fiddler、Charles等抓包工具时,需在设备上安装工具的根证书(通常通过访问特定URL下载),证书默认存储于用户证书区域。
CA证书的客户端存储策略
是否需要保存?
- 必须保存的场景:
- 需长期信任自签名证书或私有CA(如企业内部系统)时,需将证书持久化到客户端。
- 需绕过系统默认信任链(如调试环境)时,需在代码中硬编码或动态加载证书。
- 无需保存的场景:
- 证书已由系统或用户手动安装到设备全局信任库(如权威CA颁发的证书),应用可直接通过系统API调用。
存储方式与安全性
- 安全存储位置:
- 系统信任库:通过
KeyChain
安装的证书受系统保护,其他应用无法直接访问。 - 应用私有目录:若仅限本应用使用,可将证书加密后存入
SharedPreferences
或Internal Storage
。
- 系统信任库:通过
- 避免明文存储:
- 证书文件或密钥需使用
AndroidKeyStore
加密,防止被逆向提取。
- 证书文件或密钥需使用
总结
操作类型 | 核心步骤 | 适用场景 | 存储必要性 |
---|---|---|---|
手动安装 | 导出/下载证书→系统弹窗确认安装 | 调试工具、私有CA信任 | 依赖系统全局存储 |
代码集成 | 证书文件嵌入assets →动态加载 | 自签名服务器、固定证书场景 | 需应用内加密存储 |
抓包工具配置 | 安装工具根证书→指定代理 | 网络调试、HTTPS流量分析 | 临时存储于用户区域 |
最佳实践:优先通过系统信任机制管理CA证书,仅在必要时将证书保存至客户端私有目录并严格加密。
15. 安卓内存和磁盘的缓存机制?
Android 内存与磁盘缓存机制解析
结合主流框架(如 Glide、Fresco)和系统原生方案(LruCache、DiskLruCache),Android 缓存机制的核心设计如下:
1. 内存缓存
-
核心组件:
- LruCache:基于 最近最少使用(LRU) 算法管理强引用缓存,固定容量下自动淘汰旧数据,适用于高频访问的 Bitmap 等对象。
- 弱引用缓存:用于保存正在使用的资源(如 Glide 的
ActiveResources
),防止活跃对象被 LruCache 回收,优先保障当前业务需求。
-
实现要点:
- 容量计算:通常分配应用可用内存的 1/8 或系统总内存的 1/4(如
Runtime.getRuntime().maxMemory()/8
)。 - 性能优化:通过
BitmapFactory.Options.inMutable
实现 Bitmap 复用,减少内存抖动。
- 容量计算:通常分配应用可用内存的 1/8 或系统总内存的 1/4(如
2. 磁盘缓存
-
核心组件:
- DiskLruCache:基于文件的 LRU 缓存,支持持久化存储大容量数据(如图片、网络响应),避免重复下载或计算。
- 文件与数据库:小规模数据可直接用
File
存储;需高效检索或结构化数据时采用 SQLite 等数据库。
-
实现要点:
- 存储路径:优先使用
Context.getCacheDir()
私有目录,防止其他应用访问。 - 缓存策略:
- 结合 HTTP 头(如
Cache-Control
)控制缓存有效期。 - 通过
journal
日志文件记录缓存操作,保障数据一致性。
- 结合 HTTP 头(如
- 存储路径:优先使用
3. 二级缓存协作机制
-
数据请求流程(以图片加载为例):
- 内存优先:检查 LruCache → 弱引用缓存 → 返回已加载资源。
- 磁盘兜底:若内存未命中,从 DiskLruCache 读取;若仍无数据,触发网络请求并回填两级缓存。
- 缓存更新:新数据写入弱引用缓存后,同步更新 LruCache 和 DiskLruCache。
-
优势:
- 减少 I/O 开销:90% 以上高频数据通过内存直接获取,响应速度提升 10~100 倍。
- 降低网络依赖:离线场景可通过磁盘缓存恢复数据。
4. 缓存策略对比
维度 | 内存缓存 | 磁盘缓存 |
---|---|---|
存储介质 | RAM | 闪存或 SD 卡 |
容量限制 | 系统内存的 1/4(动态调整) | 固定上限(如 100MB) |
访问速度 | 纳秒级(直接内存读写) | 毫秒级(需文件解析) |
典型应用 | Bitmap、高频小对象 | 网络响应、大文件、历史数据 |
5. 最佳实践
-
内存优化:
- 监控
onTrimMemory()
事件,动态释放非活跃缓存。 - 避免使用已废弃的
SoftReference
,改用 LruCache 强引用管理。
- 监控
-
磁盘优化:
- 定期清理过期文件(通过
DiskLruCache.trimToSize()
)。 - 对敏感数据加密存储(如结合
AndroidKeyStore
)。
- 定期清理过期文件(通过
总结:Android 通过内存与磁盘缓存的协同设计,在保障数据时效性的同时显著提升性能。开发者需根据业务场景灵活配置缓存策略,平衡资源占用与用户体验。
16. 是否用过大模型,是否会使用大模型写代码?
大模型在代码开发中的应用现状与使用策略
结合当前(2025年4月)技术发展和实践案例,大模型在代码编写中的使用已形成明确趋势,但其应用需结合人工干预和策略优化。以下是关键分析:
1. 大模型写代码的典型应用场景
- 代码补全与模板生成:
大模型(如CodeFuse、GPT系列)可基于上下文自动预测后续代码,显著减少重复性输入,例如通过输入关键词快速生成函数框架或类定义。 - 快速实现简单逻辑:
对于通用功能(如文件操作、API调用),大模型可生成符合语言规范的代码片段,开发者仅需微调即可使用。 - 辅助学习与调试:
开发者可通过大模型快速获取语法示例、错误修复建议或代码优化方案,降低学习成本。
2. 大模型提升开发效率的核心优势
- 加速原型开发:
大模型可快速生成功能模块的初始代码,将开发者的精力集中在业务逻辑设计而非基础实现上。 - 减少低级错误:
模型生成的代码通常符合语法规范,可规避拼写错误、参数顺序混淆等常见问题。 - 跨语言支持:
开发者可通过自然语言描述需求,直接生成多语言(Python、Java等)的等效代码,降低语言切换成本。
3. 当前大模型写代码的局限性
- 复杂业务逻辑理解不足:
大模型难以精准捕捉特定领域(如金融风控、医疗算法)的复杂规则,生成的代码常需人工调整。 - 上下文关联性有限:
在大型项目中,模型可能忽略全局变量、依赖库版本等上下文信息,导致生成的代码与现有架构不兼容。 - 知识更新滞后:
若编程语言或框架发布新特性(如Python 3.12的语法改进),模型可能沿用旧版写法,需开发者手动修正。
4. 大模型驱动的开发流程变革
- “模型优先”原则:
优先尝试用大模型生成代码,仅在其无法满足需求时再手动编码,以降低开发成本。 - 人机协作模式:
开发者需扮演“代码审查者”角色,重点验证生成代码的安全性、性能及业务匹配度,而非完全依赖模型。 - 迭代式优化:
通过多次与大模型交互(如细化需求描述、补充约束条件),逐步逼近目标代码。
5. 高效使用大模型写代码的技巧
- 精准描述需求:
提供清晰的输入输出示例、边界条件和异常处理要求,例如:“用Python实现一个函数,输入为整数列表,返回去重且按降序排列的新列表,需处理空输入异常”。 - 角色化提问:
指定模型角色(如“资深Java后端工程师”)以生成更专业的代码。 - 分步骤拆解任务:
将复杂需求分解为多个子任务,分阶段生成并整合代码,例如先定义接口再实现具体类。
总结与建议
场景 | 推荐策略 |
---|---|
通用功能实现 | 直接使用大模型生成代码并微调 |
复杂业务逻辑开发 | 人工设计核心逻辑,大模型辅助填充工具函数 |
团队协作项目 | 结合代码规范文档约束模型输出,确保一致性 |
核心结论:大模型已成为开发者的高效辅助工具,但其无法完全替代人工编码。开发者需掌握模型交互技巧,平衡自动化生成与人工设计,以实现效率与质量的双重提升。