乐趣区

关于android:浅谈App响应时间优化

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

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

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

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

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

退出移动版