Code前端首页关于Code前端联系我们

应用程序开发:如何优化响应时间?

terry 2年前 (2023-09-22) 阅读数 83 #移动小程序

响应时间的长短会影响用户对某个功能、某个应用程序、甚至某个系统的使用。毕竟,只要有选择,没有人会愿意使用落后的应用程序或速度慢的手机。

作为一名开发者,即使我们可能只关注堆的业务,没有时间和机会去优化我们程序的响应时间,但这些内容对于我们个人的技术成长来说是必不可少的。不说大事,这部分也是采访中经常探讨的内容。如果你知道这一点,你就不会受苦。

那么让我们长话短说,快速了解如何优化应用程序的响应时间。

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 优先级任务

与线程优先级类似,当系统资源不足时,优先级较高的线程会先执行。

首先我们需要对应用程序中所有需要优化的作业及其子任务进行优先级排序,然后按优先级顺序调度和执行它们。

那么如何保证任务按优先级执行呢?

1。对于线程,我们可以直接设置它的优先级值。 (但一般情况下,我们不能直接使用线程,所以可以忽略)
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。大文件或长网页视图的分段加载:一些阅读器应用程序经常遇到大文件和长网页视图的加载。这里也可以用同样的方式来划分。

2.2.3 预加载

分段加载常与预加载结合使用。对于一些需要较长时间加载的内容,我们可能会延长加载时间,以减少用户感知的加载时间。

预加载的本质就是提前加载,所以这个提前加载的时机非常关键和重要。因为如果预加载时间太晚,几乎没有效果;但如果预加载时间太早,会抢占其他模块的资源,造成资源紧张。

那么什么时候可以开始预载,预载时间是多少呢?下面我举一些简单的例子。

1。当用户行动时。如果用户点击第 2 章,我们就会开始预加载下一章和上一章;当用户滚动到第 3 页时,我们预加载第 4 页;当用户向下滚动到第 5 页时,我们预加载第 4 页。

2。当应用程序空闲时。比如前面提到的IdleHandler。或者在onUserInteraction中监控用户的操作。如果一段时间内不起作用,则视为不活动。

3。漫长的等待。对于一些常见的耗时操作,我们最初可以并行执行一些预取操作,以提高时间利用率。例如,创建活动非常耗时。我们可以在startActivity之前开始预加载数据。这样,在创建activity之后,就可以加载数据并用于直接渲染。例如,一些带有开屏广告的应用程序可能会在广告开始时同步预加载一些数据源。

2.3 数据结构

  • 1.数据结构优化(空间大小、读取速度、可重用性、可扩展性)。
  • 2。数据缓存(内存缓存、磁盘缓存、网络缓存)、分段缓存。您可以参考这里滚动。
  • 3.锁优化(减少过多的锁,避免死锁),悲观锁/乐观锁。
  • 4。内存优化,避免内存抖动、频繁GC(特别注意位图)

2.3.1 数据结构优化

不同的数据结构有不同的使用场景。通过选择正确的数据结构,可以事半功倍。

1。 ArrayList和LinkedList:

  • ArrayList:基本数据结构是数组,查询快,添加删除慢。
  • LinkedList:基本数据结构是链表,查询慢,增删快。?但扩展对性能要求很高,空间利用率不高(75%),浪费内存。
  • SparseArray:基本数据结构是一个double数组,一个数组存储key,一个数组存储value。当数据量较小(一百个元素以下)时使用二进制查询进行优化,速度相当于HashMap,但空间利用率大大提高。
  • ArrayMap:基本数据结构是双精度数组。一个数组保存键的哈希值,另一个数组保存该值。设计与SparseArray类似,在数据量较小时可以完全替代HashMap。

3.设置:确保每一项都必须是唯一的。

4.TreeSet和TreeMap:有序集合,保证存储的元素是有序的,比HashSet和HashMap慢。

可以看出,无论空间利用率如何,HashMap 性能都很好。

但是由于初始化大小和扩展因子影响我们使用时的性能,所以我们尽量根据实际需要设置合理的初始化大小:避免设置太小导致扩展容量和设置时产生容量消耗较大的设置并导致空间损失。

由于HashMap的默认扩展因子是0.75,如果你实际使用的数字是8,那么你的初始大小设置为16;如果你实际使用的数字是60,那么你的初始大小设置为128。

2.3.2 数据缓存

对于一些不经常变化的数据源,我们可以缓存它们。这样,下次我们需要使用它们时,我们可以直接读取缓存,大大减少了加载和渲染的时间。

一般意义上的缓存按照读取时间从快到慢可以分为内存缓存、磁盘缓存和网络缓存。

  • 内存缓存存储在内存中,可以直接读取使用。从界面渲染的角度来看,内存缓存可以分为活动(活动/显示)缓存和非活动(不活动/不显示)缓存。
  • 磁盘缓存存储在磁盘上的文件中。每次读取时,必须将磁盘文件的内容读入内存后才能使用。
  • 网络缓存存储在远程服务器上。每次读取都需要我们发出网络请求。一般来说,我们也可以将网络缓存所需的数据缓存到磁盘上,将网络缓存转换为磁盘缓存,通过降低网络要求来提高读取速度。

从某种意义上来说,内存缓存、磁盘缓存和网络缓存是可以相互转换的。一般来说,我们会使用网络缓存->磁盘缓存->内存缓存来提高读取速度。

具体可以参考滚动框架和RecyclerView的实现原理。

2.3.3 锁优化

锁是处理并发的重要手段。但如果锁被滥用,很可能会导致执行效率的下降,更严重的是,可能会造成死锁等不可逆的场景。

当我们要应对高并发场景时,同步调用尤其要考虑由于加锁带来的性能损失:

  • 如果可以使用非锁定数据结构,就不要使用锁定。
  • 缩小锁。如果可以锁定块,就不要锁定方法体;如果可以使用对象锁,就不要使用类锁。

那么具体应该做什么呢?下面我简单说几个例子。

1。使用乐观锁代替悲观锁,使用轻型锁代替重型锁。

采用机制CAS,全称是Compare And Swap,意思是先比较,再交换。这意味着每次执行或更改变量时,我们都会比较新旧值,如果存在偏移则更新它们。这类似于一些无锁数据库,其中每个数据库操作都将带有唯一的版本号。每次修改数据库时,都会比较数据库记录和操作请求的版本号。如果版本号是最新版本号,则更改,否则丢弃。

需要注意的是,CAS必须使用volatile来读取最新的共享变量值才能达到【比较替换】的效果,因为 具有挥发性。 将提供可变的可见性。

在Java中,JDK为我们提供了一些原子类,这些类默认通过CAS机制实现,例如AtomicIntegerAto mic参考

2。最小化同步范围并避免直接使用synced。即使使用它,也尽量使用同步块而不是同步方法。使用JDK提供的几个同步工具:CountDownLatch、CyclicBarrier、ConcurrentHashMap。

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

  • 对于并发读多写少的情况,我们可以使用读写锁(多个读锁不互斥,读写锁互斥):ReentrantReadWriteLock、CopyOnWriteArrayList、CopyOnWriteArraySet。
  • 当并发操作通常由特定线程执行时,可以尝试使用偏向锁(偏向于第一个获取它的线程)。
  • 对于并发资源争用较多的场景,建议使用重度同步锁。

2.3.4 内存优化

内存优化的本质是避免内存抖动。不合理的内存分配、内存泄漏以及对象的频繁创建和销毁都会引起内存抖动,最终导致系统频繁GC。

频繁的GC肯定会导致系统性能的下降。更严重的情况下,可能会导致页面冻结,导致用户体验不佳。那么我们应该从哪里开始优化呢?

  • 修复应用中的内存泄漏问题。在这里,我们可以使用 LeakCanary 或 Android Profile 等工具来检查查询中可能存在的内存泄漏。
  • 编码时要小心,避免内存泄漏。例如,避免全局静态变量和常量、具有资源对象的单个元素(Activity、Fragment、View等)、使用后立即释放或回收资源等。
  • 避免创建大内存对象以及频繁创建和释放对象(尤其是在循环体中)。频繁创建的对象必须被重用或缓存。
  • 上传图像可能会相应降低图像质量。尝试对小图标使用 SVG,并考虑对大/复杂图像使用 webp。尝试使用像 glide 这样的图像加载框架,因为这些框架将帮助我们优化加载。
  • 避免绘制大量位图。 ?在 中创建对象。
  • 使用SpareArray和ArrayMap代替HashMap。
  • 避免大量的字符串操作,尤其是序列化和反序列化。请勿使用 +(加号)来连接字符串。
  • 使用线程池(可以设置合适的最大线程组数)来运行线程任务,防止大量线程的创建和泄漏。

2.4 线程/IO

  • 1。线程优化(统一、优先级、任务特性)
  • 2. IO优化(网络IO和磁盘IO),核心是减少IO次数
    • 网络:合并请求、请求链接优化、请求体优化、序列化和反序列化优化、请求复用等。
    • 磁盘:随机读写文件、SharePreference读写等。 (比如读多写少,可以使用缓存)
  • 3。日志优化(循环日志打印、不必要的日志打印、日志级别)

2.4.1 线程优化

当我们创建线程时,需要向系统请求资源并分配内存空间。这是一笔很大的成本,所以我们在正常的开发过程中并不直接管理它。线程,而是选择使用线程池来运行任务。因此,线程优化的本质是优化线程池。

使用线程池最大的问题是,如果池设置不正确,很容易被滥用并导致内存溢出问题。通常,一个应用程序有多个线程组。不同的函数、不同的模块、甚至不同的第三方库都会有自己的线程池。所以大家都用自己的,很难协调统一资源,不能在同一个地方工作。去做。

那么我们应该如何优化线程池呢?

1。建立主线程池+辅线程池的组合线程池,由线程池管理器协调管理。主线程池负责较高优先级的任务,而辅助线程池负责低优先级的任务以及被主线程池拒绝和降级的任务。

此处执行的任务必须优先考虑任务。任务的优先级调度是通过PriorityBlockingQueue队列执行的。以下是主、副线程池设置,仅供参考:

  • 主线程池:core 线程数及最大线程数: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。优化网络要求。

  • 避免不必要的网络请求。对于不必要的网络请求,可以推迟请求或者使用缓存。
  • 优化集成需要多个串行网络请求的接口,控制请求接口碎片。例如,后端具有检索用户信息的接口、检索用户推荐信息的接口、检索用户账户信息的接口。这三个接口都是必要的接口,并且有先后顺序关系。如果连续发出3个请求,时间本质上都花在了网络传输上,尤其是在网络不稳定的情况下。但如果将这三个接口综合起来获取用户启动(初始化)的信息,将大大节省网络中的数据传输时间,同时也可以提高接口的稳定性。

2。磁盘IO优化

  • 避免不必要的磁盘IO操作。这里的磁盘IO包括:读写文件、读写数据库(sqlite)、SharePreference等。
  • 选择适当的数据结构来加载数据。您可以选择支持随机读写和延迟解析的数据存储结构来替代SharePreference。
  • 避免程序执行过程中进行大量的序列化和反序列化(这会导致大量的对象创建)。 ?最大限度地减少嵌套并避免过度渲染(背景)(连接点、ConstraintLayout)
  • 2。页面重用(包括)
  • 3。页面加载缓慢
  • 4。缓慢加载布局(ViewStub)
  • 5。 inflate优化(布局预加载+异步加载,新增动态控制/。自定义视图优化(减少对象创建和onDraw、onLayout、onMeasure运行时间)
  • 8.位图和canvas优化(位图大小、质量、压缩、复用);canvas重用:clipRect,翻译)
  • 9。 RecycleView优化(减少刷新次数,缓存重用)

3.推荐工具

  • systrace、Perfetto、Android Profile
  • DoKit
  • LeakCanary
  • 成功

终于

还是这句话胜于见百遍,试一次胜于见一百次。写了这么多,还是希望大家在平时的开发过程中多关注一些与优化应用响应时间相关的技巧,这样我们才能开发出流畅、流畅的应用。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门