呼应时刻,它是用来衡量体系运转功率的一个重要指标。评价一个运用的呼应时刻,能够从用户感知和体系功能这两个视点来考量。

呼应时刻的长短,或许影响用户对某个功能、某个运用、乃至某个体系的运用。毕竟假如有挑选,没有哪个人会愿意去运用卡顿的运用,运转慢的手机。

作为一名开发者,尽管咱们平常或许只注重于堆事务,根本就没有时刻或许机会去优化咱们程序的呼应时刻,可是这些内容对咱们个人的技能成长是至关重要的。大的不说,这部分也是面试中经常考察的内容,知道了也不至于吃亏。

那么接下来咱们就长话短说,赶忙来瞧瞧,究竟怎么来优化咱们运用的呼应时刻。

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开源之旅】

本文正在参与「金石计划」