响应工夫,它是用来掂量零碎运行效率的一个重要指标。评估一个利用的响应工夫,能够从用户感知和零碎性能这两个角度来考量。

响应工夫的长短,可能影响用户对某个性能、某个利用、乃至某个零碎的应用。毕竟如果有抉择,没有哪个人会违心去应用卡顿的利用,运行慢的手机。

作为一名开发者,尽管咱们平时可能只关注于堆业务,基本就没有工夫或者机会去优化咱们程序的响应工夫,然而这些内容对咱们集体的技术成长是至关重要的。大的不说,这部分也是面试中常常考查的内容,晓得了也不至于吃亏。

那么接下来咱们就长话短说,连忙来瞧瞧,到底如何来优化咱们利用的响应工夫。

1. 外围准则

在算法中,咱们常常会从工夫复杂度空间复杂度这两个纬度来掂量算法的优劣。

很多时候,咱们无奈做到工夫复杂度空间复杂度两者都最佳,只能在"工夫"和"空间"中,取折中的最优解。同样的,如果咱们谋求最极致的"工夫"最佳,就可能须要就义一部分的"空间",这就是拿"空间"换"工夫"的解法。

响应工夫优化的外围:空间 -> 工夫 (用空间换工夫)

那么咱们应该怎么做呢?上面是我演绎总结进去的四项根本准则:

  • 1.缓存优先:能读缓存读缓存。
  • 2.缩小新建:能复用绝不新建。
  • 3.缩小工作:能不做的尽量不做。
  • 4.具体问题具体分析:针对具体事务自身进行剖析,必须做的能提前做就提前做,不必须做的延后做。

2. 优化措施

可能我下面说的这些外围和根本准则,对绝大多数人来说都十分好了解,然而晓得了这些,并不代表你懂得如何进行优化。 这就好比你高中学数学,即使通知了你一堆的公式,但真要让你来一道相干的应用题,你还真不肯定能解得进去,这个时候"例题"就很要害了。

同样的,即使你晓得了一些对于利用响应工夫优化的外围和准则后,当你真正面临具体的优化问题时,你可能也会不知所措。

所以,接下来我就从工作执行资源加载数据结构线程/IO页面渲染这五个角度,来给出我的优化倡议。

2.1 工作执行

  • 1.业务/工作梳理:对业务进行拆分,对工作进行整合。
  • 2.工作转换:串行 -> 并行, 同步 -> 异步。
  • 3.执行程序按优先级调整。
  • 4.提早执行、闲暇执行,如:IdleHandler

2.1.1 业务/工作梳理

业务往往是由一个个工作流组合而成。正当的业务/工作粒度能够无效进步响应的速度。

对业务和工作的梳理,正确的形式是先进行业务的拆分,将业务拆分为一个个子工作,再依据须要对子工作进行整合。

(1)对不合理的业务流进行拆分。

  • 对业务进行拆分,拆分出次要(必要)业务和主要(非必要)业务。
  • 别离对次要业务和主要业务进行优先级评估,业务执行按优先级从高到底顺次执行。

(2)对工作流进行整合。

  • 多个相干的串行工作,能够整合为对立的业务整体。
  • 多个不相干的串行工作,能够整合为一个并行的业务。

2.1.2 工作转换

1.串行 -> 并行的适用范围:

  • 多个不相干的串行工作。
  • 多个工作弱相干且耗时,然而耗时靠近。例如某个页面你须要调用多个模块的接口查问数据进行展现。

2.同步 -> 异步的适用范围:

  • 非必要(重要性不高)且耗时的工作。
  • 耗时且关联性不大的工作。
  • 耗时且存在肯定相关性的工作。应用异步线程 + 同步锁的形式执行。

2.1.3 工作优先级

相似线程中的优先级Priority,当系统资源缓和的时候,优先执行优先级高的线程。

首先咱们要对利用内所有须要优化的业务以及其子工作的优先级进行定义,而后按优先级程序进行排列和执行。

那么如何能力保障工作被按优先级进行执行呢?

1.对于线程,咱们能够间接设置其Priority值。(然而个别咱们不能间接应用线程,所有这个能够疏忽)
2.对于线程池,咱们能够从代码层将工作按优先级程序退出到线程池中。留神,这里的线程池最好是阻塞式的,例如:应用PriorityBlockingQueue实现的优先级线程池 PriorityThreadPoolExecutor 。
3.应用第三方的工作执行框架,这里举荐我开源的 XTask 供大家参考。

2.1.4 提早执行

提早执行,是将一些不必要、重要性不高或者高耗时的工作暂停执行,等前面资源短缺或者要应用时才执行。

常见的提早执行有以下几种:

  • 提早某个特定的工夫执行。例如:某利用启动后,每隔2分钟同步一下用户状态。
  • 待某个特定的工作执行实现之后再执行。例如:导航利用定位获取胜利后,再执行目的地举荐获取的工作。
  • 间接不执行,等相干业务用到的时候再执行。
  • 闲暇执行,期待页面都齐全渲染结束之后再执行。例如:应用IdleHandler,具体应用如下:
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {    @Override    public boolean queueIdle() {        // 执行你的工作        return false;    }});

当然,如果你想在闲暇的时候执行多个工作,你也能够这样写:

public class DelayTaskQueue {  private final Queue<Runnable> mDelayTasks = new LinkedList<>();  private final MessageQueue.IdleHandler mIdleHandler = () -> {    if (mDelayTasks.size() > 0) {      Runnable task = mDelayTasks.poll();      if (task != null) {        task.run();      }    }    // mDelayTasks非空时返回ture示意下次继续执行,为空时返回false零碎会移除该IdleHandler不再执行    return !mDelayTasks.isEmpty();  };  public DelayTaskQueue addTask(Runnable task) {    mDelayTasks.add(task);    return this;  }  public void start() {    Looper.myQueue().addIdleHandler(mIdleHandler);  }}

2.2 资源加载

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

2.2.1 懒加载

对于一些不罕用或者不重要的数据、图片、控件以及其余一些资源,咱们能够在用到时再进行加载。

1.数据懒加载

  • kotlin中的lazy标签:润饰val变量,程序第一次应用到这个变量(或者对象)时再初始化。
  • Map、List和SharedPreferences等大数据的提早初始化。

    private Map getSystemSettings() {  if (mSettingMap == null) {      mSettingMap = initSystemSettings();  }  return mSettingMap;}

2.图片资源懒加载

  • 对于不罕用的图片,能够应用云端图片的资源url来代替。
  • 对于非程序预置的图片(本地图片文件或者云端图片),用到时再加载。

3.控件懒加载

  • 应用ViewStub进行布局的提早加载。
  • 应用ViewPager2+Fragment进行Fragment的懒加载。
  • 应用RecyclerView代替ListView。

2.2.2 分段加载

分段加载常见利用于大数据的加载,这里包含大图和长视频等多媒体资源的加载。做到用到哪,加载到哪,齐全不必要等全副加载完才给用户应用。

1.大图的分段加载:对于大图,咱们能够将其按肯定尺寸进行切分,宰割成一块一块的小瓦片,而后设定一个预览预加载范畴,用户预览到哪里咱们就加载到哪里。(就相似地图的加载)

2.长视频的分段加载:对于长视频,咱们能够将其按工夫片进行拆分,并设置一个加载缓存池。这样用户浏览一个长视频时,就能够疾速关上加载。

3.大文件或者长WebView的分段加载:对于一些浏览类的app,常常会遇到大文件和长WebView的加载,这里咱们也能够同理对其进行拆分解决。

2.2.3 预加载

分段加载常和预加载一起组合应用。对于一些加载十分耗时的内容,咱们能够将加载机会提前,从而减小用户感知的加载工夫。

预加载的实质是提前加载,这样这个提前加载的机会就十分的要害和重要。因为预加载机会如果太晚,简直看不出成果;然而如果预加载的机会过早,有可能抢占其余模块资源,造成资源缓和。

那么咱们何时能够触发预加载,预加载的机会是什么呢?上面我举几个简略的例子。

1.用户操作时。如果用户点击了第2章,咱们就开始预加载下一章和上一章;用户上滑到了第3页,咱们预加载第4页,用户下滑到第5页,咱们预加载第4页.

2.利用闲暇时。例如之前说的IdleHandler。或者在onUserInteraction中监听用户的操作,一段时间没有操作即视为闲暇。

3.耗时期待时。对于一些常见的耗时操作,咱们能够在其开始时,并行进行一些预加载操作,从而进步工夫的利用率。例如Activity的创立比拟耗时,咱们能够在startActivity前就开始预加载数据,这样Activity创立完之后有可能数据就曾经加载好了,间接能够拿来渲染。例如一些有开屏广告的app,能够在广告开始时,同步进行一些数据资源的预加载。

2.3 数据结构

  • 1.数据结构优化(空间大小、读取速度、复用性、扩展性)。
  • 2.数据缓存(内存缓存、磁盘缓存、网络缓存),分段缓存。这里能够参考glide.
  • 3.锁优化(缩小适度锁,防止死锁),乐观锁/乐观锁。
  • 4.内存优化,防止内存抖动,频繁GC(尤其关注bitmap)

2.3.1 数据结构优化

不同的数据结构有不同的应用场景,抉择适宜的数据结构可能事倍功半。

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。

能够看到,在不思考空间利用率的状况下,HashMap的性能是不错的。

然而因为存在初始化大小和扩大因子对其性能有所影响,咱们在应用时,尽量依据理论须要设置正当的初始化大小:防止设置小了扩容带来性能耗费,设置大了造成空间节约。

因为HashMap的默认扩容因子是0.75,如果你理论应用的数量是8,那你初始化大小就设置16;如果你理论应用的数量是60,那你初始化大小就设置128。

2.3.2 数据缓存

对于一些变动不是很频繁的数据资源,咱们能够将其缓存下来。这样咱们下次须要应用它们的时候,就能够间接读取缓存,这样极大地缩小了加载和渲染所须要的工夫。

个别意义上的缓存,按读取的工夫由快到慢,咱们可分为内存缓存、磁盘缓存、网络缓存。

  • 内存缓存,就是存储在内存中,咱们能够间接读取应用。而如果从界面渲染的角度,咱们又能够将内存缓存分为Active(沉闷/正在显示)缓存和InActive(非沉闷/不可显示)缓存。
  • 磁盘缓存,就是存储在磁盘文件中,每次读取都须要将磁盘文件内容读取到内存中,方可应用。
  • 网络缓存,就是存储在远端服务器中,每次读取须要咱们进行一次网络申请。一般来说,咱们也能够将一次网络缓存申请到的数据缓存到磁盘中,将网络缓存转化为磁盘缓存,通过缩小网络申请,来晋升读取速度。

某种意义上来说,内存缓存、磁盘缓存和网络缓存,它们又是能够互相转化的,一般来说,咱们会将网络缓存->磁盘缓存->内存缓存,进行应用,从而晋升读取速度。

具体咱们能够参考glide框架和RecyclerView的实现原理。

2.3.3 锁优化

锁是咱们解决并发的重要伎俩,然而如果滥用锁的话,很可能造成执行效率降落,更重大的可能造成死锁等无法挽回的场景。

当咱们须要解决高并发的场景时,同步调用尤其须要考量锁的性能损耗:

  • 能用无锁数据结构,就不要用锁。
  • 放大锁的范畴。能锁区块,就不要锁住办法体;能用对象锁,就不要用类锁。

那么咱们具体应该怎么做呢?上面我简略讲几个例子。

1.应用乐观锁代替乐观锁,轻量级锁代替重量级锁。

利用CAS机制, 全称是Compare And Swap,即先比拟,而后再替换。就是每次执行或者批改某个变量时,咱们都会将新旧值进行比拟,如果产生偏移了就更新。这就好比在一些无锁的数据库中,每次的数据库操作都会携带一个惟一的版本号,每次进行数据库批改的时候都会比照一下数据库记录和操作申请的版本号,如果版本号是最新的版本号,则进行批改,否则抛弃。

须要留神的是,CAS必须借助volatile能力读取到共享变量的最新值来实现【比拟并替换】的成果,因为volatile会保障变量的可见性。

在Java中,JDK给咱们默认提供了一些CAS机制实现的原子类,如AtomicIntegerAtomicReference等。

2.放大同步范畴,防止间接应用synchronized,即便应用也要尽量应用同步块而不是同步办法。多应用JDK提供给咱们的同步工具:CountDownLatch,CyclicBarrier,ConcurrentHashMap。

3.针对不同应用场景,应用不同类型的锁。

  • 针对并发读多,写少的,咱们能够应用读写锁(多个读锁不互斥,读锁与写锁互斥):ReentrantReadWriteLock,CopyOnWriteArrayList,CopyOnWriteArraySet。
  • 针对某一个并发操作通常由某一特定线程执行时,可尝试应用偏差锁(偏差于第一个取得它的线程)。
  • 针对存在大量并发资源竞争的场景,举荐应用重量级锁synchronized。

2.3.4 内存优化

内存优化的外围是防止内存抖动。不合理的内存调配、内存透露、对象的频繁创立和销毁,都会导致内存产生抖动,最终导致系统的频繁GC。

频繁的GC,必定会导致系统运行效率的降落,重大的可能会导致页面卡顿,造成不好的用户体验。那么咱们应该着手从哪些地方进行优化呢?

  • 解决利用的内存透露问题。这里咱们能够应用LeakCanary 或者 Android Profile 等工具来查看咱们查问可能存在的内存透露。
  • 平时编码该当留神防止内存透露。如防止全局动态变量和常量、单例持有资源对象(Activity,Fragment,View等),资源应用完立刻开释或者recycle(回收)等。
  • 防止创立大内存对象,频繁创立和开释对象(尤其是在循环体内),频繁创立的对象须要思考复用或者应用缓存。
  • 加载图片能够适当升高图片品质,小图标尽量应用SVG,大图/简单的图片思考应用webp。尽量应用图片加载框架,如glide,这些框架都会帮咱们进行加载优化。
  • 防止大量bitmap的绘制。
  • 防止在自定义View的onMeasureonLayoutonDraw中创建对象。
  • 应用SpareArray、ArrayMap代替HashMap。
  • 防止进行大量的字符串操作,特地是序列化和反序列化。不要应用+(加号)进行字符串拼接。
  • 应用线程池(可设置适当的最大线程池数)执行线程工作,防止大量Thread的创立及透露。

2.4 线程/IO

  • 1.线程优化(对立、优先级调度、工作个性)
  • 2.IO优化(网络IO和磁盘IO),外围是缩小IO次数

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

2.4.1 线程优化

当咱们创立一个线程时,须要向零碎申请资源,分配内存空间,这是一笔不小的开销,所以咱们平时开发的过程中都不会间接操作线程,而是抉择应用线程池来执行工作。所以线程优化的实质是对线程池的优化。

线程池应用的最大问题就在于如果线程池设置不对的话,很容易被人滥用,引发内存溢出的问题。而且通常一个利用会有多个线程池,不同性能、不同模块乃至是不同三方库都会有本人的线程池,这样大家各用各的,就很难做到资源的协调对立,劲不往一处使。

那么咱们应该如何进行线程池优化呢?

1.建设主线程池+副线程池的组合线程池,由线程池管理者对立协调治理。主线程池负责优先级较高的工作,副线程池负责优先级不高以及被主线程池回绝降级下来的工作。

这里执行的工作都须要设置优先级,工作优先级的调度通过PriorityBlockingQueue队列实现,以下是主副线程池的设置,仅供参考:

  • 主线程池:外围线程数和最大线程数:2n(n为CPU外围数),60s keepTime,PriorityBlockingQueue(128)。
  • 副线程池:外围线程数和最大线程数:n(n为CPU外围数),60s keepTime,PriorityBlockingQueue(64)。

2.应用Hook的形式,收集利用内所以应用newThread办法的中央,改为由线程池管理者对立协调治理。

3.将所有提供了设置线程池接口的第三方库,通过其凋谢的接口,设置为线程池管理者治理。没有提供设置接口的,思考替换库或者插桩的形式,替换线程池的应用。

2.4.2 IO优化

IO优化的外围是缩小IO次数。

1.网络申请优化。

  • 防止不必要的网络申请。对于那些非必要执行的网络申请,能够延时申请或者应用缓存。
  • 对于须要进行屡次串行网络申请的接口进行优化整合,管制好申请接口的粒度。比方后盾有获取用户信息的接口、获取用户举荐信息的接口、获取用户账户信息的接口。这三个接口都是必要的接口,且存在先后关系。如果顺次进行三次申请,那么工夫基本上都花在网络传输上,尤其是在网络不稳固的状况下耗时尤为显著。但如果将这三个接口整合为获取用户的启动(初始化)信息,这样数据在网络中传输的工夫就会大大节俭,同时也能进步接口的稳定性。

2.磁盘IO优化

  • 防止不必要的磁盘IO操作。这里的磁盘IO包含:文件读写、数据库(sqlite)读写和SharePreference等。
  • 对于数据加载,抉择适合的数据结构。能够抉择反对随机读写、延时解析的数据存储构造以代替SharePreference。
  • 防止程序执行呈现大量的序列化和反序列化(会造成大量的对象创立)。

2.5 页面渲染

上面是我简略列举的几点放慢页面渲染的办法,置信大家或多或少都用过,这里我就不具体论述了:

  • 1.升高布局层级、缩小嵌套、防止适度渲染(背景)(merge,ConstraintLayout)
  • 2.页面复用(include)
  • 3.页面懒加载
  • 4.布局提早加载(ViewStub)
  • 5.inflate优化(布局预加载+异步加载,动静new控件/X2C)
  • 6.动画优化(留神动画的执行耗时和内存占用,不可见时暂停动画,可见时再复原动画)
  • 7.自定义view优化(缩小onDraw、onLayout、onMeasure的对象创立和执行耗时)
  • 8.bitmap和canvas优化(bitmap大小、品质、压缩、复用;canvas复用:clipRect,translate)
  • 9.RecycleView优化(缩小刷新次数,缓存复用)

3. 举荐工具

  • systrace、Perfetto 、Android Profile
  • DoKit
  • LeakCanary
  • performance

最初

还是那句话,百闻不如一见,百见不如一试。写了这么多,我还是心愿大家在平时开发的过程中,多器重一些利用响应工夫优化的相干技巧,让咱们开发出晦涩顺滑的利用吧。(只管很多时候,咱们所谓的优化会被产品或者设计diss)

我是xuexiangjys,一枚酷爱学习,喜好编程,勤于思考,致力于Android架构钻研以及开源我的项目教训分享的技术up主。获取更多资讯,欢送微信搜寻公众号:【我的Android开源之旅】