卡顿优化方法论
卡顿优化方法论
卡顿优化
卡顿优化方法论
卡顿优化,也就是提升 APP 的响应时间,用空间换时间,总结了四项基本原则:
- 缓存优先,有缓存读缓存
- 减少新建,能复用就不新建
- 减少任务,能不做的尽量不做
- 具体问题具体分析:针对具体事务本身进行分析,必须做的能提前做就提前做,不必须的延后做
优化措施
任务执行
- 任务优先级排序,优先高优先级的执行
- 线程设置
Priority
- 线程池按优先级加入线程池,最好用阻塞式:
PriorityBlockingQueue
,可参考 XTask
- 线程设置
- 将一些不重要的任务延迟执行
- 延迟几秒或几分钟执行
- 不必要的任务不执行,需要用到的时候再去执行
- 空闲执行,Idlehandler
资源加载
- 懒加载
- 分段加载(部分加载)
- 预加载(数据、布局页面等)
懒加载
- 数据懒加载
- kotlin 的 lazy
- Map、List 和 Sp 大数据延迟初始化
- 图片资源懒加载
- 控件懒加载
- ViewStub
- ViewPager 2+Fragment,Fragment 懒加载
- RecyclerView
分段加载
- 大图的分段加载
- 长视频分段加载
- 大文件或长 WebView 分段加载
预加载
- 提前加载
- IdleHandler 时加载
数据结构
- 1.数据结构优化(空间大小、读取速度、复用性、扩展性)。
- 2.数据缓存(内存缓存、磁盘缓存、网络缓存),分段缓存。这里可以参考 glide.
- 3.锁优化(减少过度锁,避免死锁),悲观锁/乐观锁。
- 4.内存优化,避免内存抖动,频繁 GC(尤其关注 bitmap)
数据结构优化
1.ArrayList 和 LinkedList:
- ArrayList:底层数据结构是数组,查询快、增删慢。
- LinkedList:底层数据结构是链表,查询慢、增删快。
2.HashMap 和 SparseArray:
- HashMap:底层数据结构是数组和链表(或红黑树)的组合,结合了 ArrayList 和 LinkedList 的优点,查询快、增删也快。但是扩容很耗性能,且空间利用率不高 (75%),浪费内存。
- SparseArray:底层数据结构是双数组,一个数组存 key,一个数组存 value。使用二分法查询进行优化,在数据量小(一百条以下)的情况下,速度和 HashMap 相当,但是空间利用率大大提升。
- ArrayMap:底层数据结构是双数组,一个数组存 key 的 hash 值,一个数组存 value。设计与 SparseArray 类似,在数据量小的情况下,可完全替代 HashMap。
3.Set: 保证每个元素都必须是唯一的。
4.TreeSet 和 TreeMap:有序的集合,保证存放的元素是排过序的,速度慢于 HashSet 和 HashMap。
数据缓存
对于一些变化不是很频繁的数据资源,我们可以将其缓存下来。这样我们下次需要使用它们的时候,就可以直接读取缓存,这样极大地减少了加载和渲染所需要的时间。
一般意义上的缓存,按读取的时间由快到慢,我们可分为内存缓存、磁盘缓存、网络缓存。
- 内存缓存,就是存储在内存中,我们可以直接读取使用。而如果从界面渲染的角度,我们又可以将内存缓存分为 Active(活跃/正在显示) 缓存和 InActive(非活跃/不可显示) 缓存。
- 磁盘缓存,就是存储在磁盘文件中,每次读取都需要将磁盘文件内容读取到内存中,方可使用。
- 网络缓存,就是存储在远端服务器中,每次读取需要我们进行一次网络请求。一般来说,我们也可以将一次网络缓存请求到的数据缓存到磁盘中,将网络缓存转化为磁盘缓存,通过减少网络请求,来提升读取速度。
某种意义上来说,内存缓存、磁盘缓存和网络缓存,它们又是可以相互转化的,一般来说,我们会将 网络缓存
->磁盘缓存
->内存缓存
,进行使用,从而提升读取速度。
具体我们可以参考 glide框架
和 RecyclerView
的实现原理。
锁优化
锁是我们解决并发的重要手段,但是如果滥用锁的话,很可能造成执行效率下降,更严重的可能造成死锁等无法挽回的场景。
当我们需要处理高并发的场景时,同步调用尤其需要考量锁的性能损耗:
- 能用无锁数据结构,就不要用锁。
- 缩小锁的范围。能锁区块,就不要锁住方法体; 能用对象锁,就不要用类锁。
具体做法:
- 使用乐观锁代替悲观锁,轻量级锁代替重量级锁。
- [[Java CAS]]
- 缩小同步范围,避免直接使用
synchronized
,即使使用也要尽量使用同步块而不是同步方法。多使用 JDK 提供给我们的同步工具:CountDownLatch
,CyclicBarrier
,ConcurrentHashMap
。 - 针对不同使用场景,使用不同类型的锁。
- 针对并发读多,写少的,我们可以使用读写锁(多个读锁不互斥,读锁与写锁互斥):
ReentrantReadWriteLock
,CopyOnWriteArrayList
,CopyOnWriteArraySet
。 - 针对某一个并发操作通常由某一特定线程执行时,可尝试使用偏向锁(偏向于第一个获得它的线程)。
- 针对存在大量并发资源竞争的场景,推荐使用重量级锁
synchronized
[[Java线程安全-锁#synchronized]]。
- 针对并发读多,写少的,我们可以使用读写锁(多个读锁不互斥,读锁与写锁互斥):
IO 优化
- 线程优化(统一、优先级调度、任务特性)
IO 优化(网络 IO 和磁盘 IO),核心是减少 IO 次数
- 网络:请求合并,请求链路优化,请求体优化,系列化和反序列化优化,请求复用等。
- 磁盘:文件随机读写、SharePreference 读写等(例如对于读多写少的,可使用内存缓存)
- log 优化(循环中的 log 打印,不必要的 log 打印,log 等级)
- 异步 IO OkIO 库
线程优化
- 使用线程池
- 统一应用内的线程池,避免一个业务一个线程池
- 建立主线程池 + 副线程池的组合线程池,由线程池管理者统一协调管理。主线程池负责优先级较高的任务,副线程池负责优先级不高以及被主线程池拒绝降级下来的任务。
- 执行的任务都需要设置优先级,任务优先级的调度通过 PriorityBlockingQueue 队列实现,以下是主副线程池的设置,仅供参考:
- 使用
Hook
的方式,收集应用内所以使用newThread
方法的地方,改为由线程池管理者统一协调管理。- 主线程池:核心线程数和最大线程数:2n(n 为 CPU 核心数),60s keepTime,PriorityBlockingQueue(128)。
- 副线程池:核心线程数和最大线程数:n(n 为 CPU 核心数),60s keepTime,PriorityBlockingQueue(64)。
- 线程池做成后端动态可配置
- 将所有提供了设置线程池接口的第三方库,通过其开放的接口,设置为线程池管理者管理。没有提供设置接口的,考虑替换库或者插桩的方式,替换线程池的使用。
IO 优化
IO 优化的核心是减少 IO 次数。
- 网络请求优化。
- 避免不必要的网络请求。对于那些非必要执行的网络请求,可以延时请求或者使用缓存。
- 对于需要进行多次串行网络请求的接口进行优化整合,控制好请求接口的粒度。比如后台有获取用户信息的接口、获取用户推荐信息的接口、获取用户账户信息的接口。这三个接口都是必要的接口,且存在先后关系。如果依次进行三次请求,那么时间基本上都花在网络传输上,尤其是在网络不稳定的情况下耗时尤为明显。但如果将这三个接口整合为获取用户的启动(初始化)信息,这样数据在网络中传输的时间就会大大节省,同时也能提高接口的稳定性。
- 磁盘 IO 优化
- 避免不必要的磁盘 IO 操作。这里的磁盘 IO 包括:文件读写、数据库(sqlite)读写和 SharePreference 等。
- 对于数据加载,选择合适的数据结构。可以选择支持随机读写、延时解析的数据存储结构以替代 SharePreference。
- 避免程序执行出现大量的序列化和反序列化(会造成大量的对象创建)。
布局
[[UI优化]]
内存优化
[[内存优化基础]]
卡顿优化分析
卡顿优化实践
mmkv 替换掉 sp
ANR free implementation of SharedPreferences.
RNoMainThreadWriteSharedPreferencesecyclerView 的优化
- 支持 DiffUtils(itemChange, payloads)
- RecyclerViewPool
- 其他?
本文由作者按照 CC BY 4.0 进行授权