文章

卡顿优化方法论

卡顿优化方法论

卡顿优化

卡顿优化方法论

卡顿优化,也就是提升 APP 的响应时间,用空间换时间,总结了四项基本原则:

  1. 缓存优先,有缓存读缓存
  2. 减少新建,能复用就不新建
  3. 减少任务,能不做的尽量不做
  4. 具体问题具体分析:针对具体事务本身进行分析,必须做的能提前做就提前做,不必须的延后做

优化措施

任务执行

  1. 任务优先级排序,优先高优先级的执行
    1. 线程设置 Priority
    2. 线程池按优先级加入线程池,最好用阻塞式:PriorityBlockingQueue,可参考 XTask
  2. 将一些不重要的任务延迟执行
    1. 延迟几秒或几分钟执行
    2. 不必要的任务不执行,需要用到的时候再去执行
    3. 空闲执行,Idlehandler

资源加载

  1. 懒加载
  2. 分段加载(部分加载)
  3. 预加载(数据、布局页面等)

懒加载

  • 数据懒加载
    1. kotlin 的 lazy
    2. Map、List 和 Sp 大数据延迟初始化
  • 图片资源懒加载
  • 控件懒加载
    1. ViewStub
    2. ViewPager 2+Fragment,Fragment 懒加载
    3. RecyclerView

分段加载

  1. 大图的分段加载
  2. 长视频分段加载
  3. 大文件或长 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 的实现原理。

锁优化

锁是我们解决并发的重要手段,但是如果滥用锁的话,很可能造成执行效率下降,更严重的可能造成死锁等无法挽回的场景。

当我们需要处理高并发的场景时,同步调用尤其需要考量锁的性能损耗:

  • 能用无锁数据结构,就不要用锁。
  • 缩小锁的范围。能锁区块,就不要锁住方法体; 能用对象锁,就不要用类锁。

具体做法:

  1. 使用乐观锁代替悲观锁,轻量级锁代替重量级锁。
    • [[Java CAS]]
  2. 缩小同步范围,避免直接使用 synchronized,即使使用也要尽量使用同步块而不是同步方法。多使用 JDK 提供给我们的同步工具:CountDownLatchCyclicBarrierConcurrentHashMap
  3. 针对不同使用场景,使用不同类型的锁。
    • 针对并发读多,写少的,我们可以使用读写锁(多个读锁不互斥,读锁与写锁互斥):ReentrantReadWriteLockCopyOnWriteArrayListCopyOnWriteArraySet
    • 针对某一个并发操作通常由某一特定线程执行时,可尝试使用偏向锁(偏向于第一个获得它的线程)。
    • 针对存在大量并发资源竞争的场景,推荐使用重量级锁 synchronized [[Java线程安全-锁#synchronized]]。

IO 优化

  1. 线程优化(统一、优先级调度、任务特性)
  2. IO 优化(网络 IO 和磁盘 IO),核心是减少 IO 次数

    • 网络:请求合并,请求链路优化,请求体优化,系列化和反序列化优化,请求复用等。
    • 磁盘:文件随机读写、SharePreference 读写等(例如对于读多写少的,可使用内存缓存)
  3. log 优化(循环中的 log 打印,不必要的 log 打印,log 等级)
  4. 异步 IO OkIO 库

线程优化

  1. 使用线程池
  2. 统一应用内的线程池,避免一个业务一个线程池
  • 建立主线程池 + 副线程池的组合线程池,由线程池管理者统一协调管理。主线程池负责优先级较高的任务,副线程池负责优先级不高以及被主线程池拒绝降级下来的任务。
  • 执行的任务都需要设置优先级,任务优先级的调度通过 PriorityBlockingQueue 队列实现,以下是主副线程池的设置,仅供参考:
  1. 使用 Hook 的方式,收集应用内所以使用 newThread 方法的地方,改为由线程池管理者统一协调管理。
    • 主线程池:核心线程数和最大线程数:2n(n 为 CPU 核心数),60s keepTime,PriorityBlockingQueue(128)。
    • 副线程池:核心线程数和最大线程数:n(n 为 CPU 核心数),60s keepTime,PriorityBlockingQueue(64)。
    • 线程池做成后端动态可配置
  2. 将所有提供了设置线程池接口的第三方库,通过其开放的接口,设置为线程池管理者管理。没有提供设置接口的,考虑替换库或者插桩的方式,替换线程池的使用。

IO 优化

IO 优化的核心是减少 IO 次数。

  1. 网络请求优化。
    • 避免不必要的网络请求。对于那些非必要执行的网络请求,可以延时请求或者使用缓存。
    • 对于需要进行多次串行网络请求的接口进行优化整合,控制好请求接口的粒度。比如后台有获取用户信息的接口、获取用户推荐信息的接口、获取用户账户信息的接口。这三个接口都是必要的接口,且存在先后关系。如果依次进行三次请求,那么时间基本上都花在网络传输上,尤其是在网络不稳定的情况下耗时尤为明显。但如果将这三个接口整合为获取用户的启动(初始化)信息,这样数据在网络中传输的时间就会大大节省,同时也能提高接口的稳定性。
  2. 磁盘 IO 优化
    • 避免不必要的磁盘 IO 操作。这里的磁盘 IO 包括:文件读写、数据库(sqlite)读写和 SharePreference 等。
    • 对于数据加载,选择合适的数据结构。可以选择支持随机读写、延时解析的数据存储结构以替代 SharePreference。
    • 避免程序执行出现大量的序列化和反序列化(会造成大量的对象创建)。

布局

[[UI优化]]

内存优化

[[内存优化基础]]

卡顿优化分析

muywy

卡顿优化实践

mmkv 替换掉 sp

ANR free implementation of SharedPreferences.

RNoMainThreadWriteSharedPreferencesecyclerView 的优化

  1. 支持 DiffUtils(itemChange, payloads)
  2. RecyclerViewPool
  3. 其他?
本文由作者按照 CC BY 4.0 进行授权