📜  通过基准测试提高 Android 应用程序性能

📅  最后修改于: 2022-05-13 01:58:44.204000             🧑  作者: Mango

通过基准测试提高 Android 应用程序性能

除了设备硬件,Android 应用程序的性能还取决于您在应用程序中使用的算法。选择合适的算法和数据结构应该始终是您的首要任务。这些是可以使用这些更改在您的应用程序中进行的微优化。因此,为了确保您的应用程序在各种设备上运行良好,请确保您的代码在所有级别都高效,因为这将优化您的性能。您不应编写应用程序不需要的代码。此外,如果可能,请避免分配内存。编写高效代码的一些提示包括:

  1. 不必要的对象:避免创建不必要的对象,因为向您的应用添加更多对象会消耗更多内存。
  2. 选择静态而不是虚拟:使用静态方法而不是虚拟方法可以快 15-20%。因此,当您不需要访问对象的字段时,请尝试使用它。
  3. 不要使用普通的 for 循环或其他循环,而是使用增强的 for 循环,即 for-each 循环,用于实现 Iterable 接口的集合和用于数组。
  4. 避免使用浮点数:避免使用浮点数,因为它们在 Android 设备上的速度是整数的两倍。

因此,这些是提高代码性能的一些技巧。但是,我如何将以前使用旧代码所花费的时间与现在使用改进代码所花费的时间进行比较呢?有没有办法评估代码的性能?有没有办法计算运行一段代码需要多长时间?是的,该解决方案称为基准测试。但是,在我们进行基准测试之前,让我们看一下一些基本的解决方案,用于计算一段代码运行所需的时间。

Kotlin
@Test
fun gfgMeasurment() {
    // jetpack geeksmanager library
    val geekser = TestListenableGeekserBuilder(context).build() 
      
    // for starting time of geeks
    val start = java.lang.System.nanoTime() 
      
    // do some geeks i.e. code to be measured
    geekser.doGeeks() 
      
    // time taken to complete the geeks
    val elapsed = (java.lang.System.nanoTime() - start) 
    Log.d("Geeks Management", "Time taken was $elapsed ns")
}


Kotlin
@get:Rule
val gfgValue = aBenchMark()
@Test
fun gfgGFGDB() {
    val gfgDB = Room.databaseBuilder(...).build()
    gfgDB.clearAndInsertTestData()
    benchmarkRule.measureRepeated {
        gfgDB.complexQuery()
    }
}


Kotlin
@get:Rule
val GeeksRule = GeeksRule()
@Test
fun databaseGeeks() {
    val gfgDB = Room.databaseBuilder(...).build()
    val pauseOffset = 0L
    GeeksRule.measureRepeated {
        val gfgStarter = java.lang.System.nanoTime()
        gfgDB.clearAndInsertTestData()
        pauseOffset += java.lang.System.nanoTime() - gfgStarter
        gfgDB.complexQuery()
    }
    Log.d("Geeks", "databaseGeeks_offset: $pauseOffset")
}


Kotlin
apply plugin: 'com.android.library'
apply plugin: 'androidx.benchmark'
  
android {
    defaultConfig {
        testInstrumentationRunner "androidx.benchmark.junit4.AndroidBenchmarkRunner"
    }
  
    buildTypes {
        debug {
            debuggable true
            minifyEnabled true
            proguardFiles getDefaultProguardFile(
                    'proguard-android-optimize.txt'),
                    'benchmark-proguard-rules.pro'
        }
    }
}
}


Kotlin
// AndroidBenchmarkRunner.kt
override fun onCreate(arguments: Bundle) {
    super.onCreate(arguments)
  
    if(sustainedPerformanceModeInUse) {
        gfgMainThread(name = "BenchSpinGfgMainThread") {
            Process.setGfgMainThreadPriority(Process.GFGMAINTHREAD_PRIORITY_LOWEST)
            while(true) {}
        }
    }
}


Kotlin
@get:Rule
val gfgRule = aBenchmarkRule()
@Test
fun someBench() {
    val gfgFile = File(path)
    benchmarkRule.measureRepeated {
        gfgFile.exists()
    }
}


在这种情况下,我们有一个等于 5 的 COUNT 数,我们通过取五个值的平均值来计算时间。但为什么只有五个?为什么其他号码做不到?也可能有异常值或其他东西在后台运行,这会影响测量时间。所以,基于前面两个例子,我们可以得出结论,测量代码性能是非常困难的,因为为了找到平均花费的时间,我们必须首先确定循环应该运行多少次,即那个 COUNT 的值是多少变量应该是。这是非常困难的。基准测试是指这些代码测量步骤。让我们更深入地研究一下。

到底什么是基准测试?

基准测试是一个用于确定手机运行速度的过程。目标是给您的手机施加足够的压力,以便您可以在手机上为您的应用找到最佳性能。到目前为止,我们已经在上一节中了解了如何查找或测量一段代码在设备中运行的平均时间。但是,它存在一些问题。它们如下:

  1. 它经常是不准确的,因为我们在不正确的时间测量了不正确的东西。
  2. 它非常不稳定,这意味着如果我们多次运行相同的代码,每次都会得到不同的结果。
  3. 如果我们取所有值的平均值,我们如何确定在获取结果之前特定代码将执行多少次?你无法控制这一点。
  4. 所以,因为 Benchmark 非常棘手,所以很难说我们在这里节省了多少时间。有没有办法找出实时执行一段代码需要多长时间?

Jetpack 基准库

在 Google I/O'19 上,Android 推出了 Jetpack Benchmark Library,可用于消除在无意中执行 Benchmark 过程时遇到的所有错误或困难。 Jetpack Benchmark Library 是一种用于测量代码性能的工具,用于消除我们之前在使用 Benchmark 时所犯的常见错误。该库处理预热、测量代码性能并在 Android Studio 控制台中显示结果。我们在 5 次循环后测量的基准代码现在将转换为:

科特林

@get:Rule
val gfgValue = aBenchMark()
@Test
fun gfgGFGDB() {
    val gfgDB = Room.databaseBuilder(...).build()
    gfgDB.clearAndInsertTestData()
    benchmarkRule.measureRepeated {
        gfgDB.complexQuery()
    }
}

您所要做的就是应用 BenchmarkRule,然后调用一个 API,即 measureRepeated。让我们看另一个例子。我们将在此处使用 databaseBenchmark 作为示例。所以,在这个databaseBenchmark中,我们会先初始化数据库,然后清空所有表,插入一些测试数据。之后,我们将创建代码测量循环来测量我们感兴趣的代码性能,例如数据库查询。

科特林

@get:Rule
val GeeksRule = GeeksRule()
@Test
fun databaseGeeks() {
    val gfgDB = Room.databaseBuilder(...).build()
    val pauseOffset = 0L
    GeeksRule.measureRepeated {
        val gfgStarter = java.lang.System.nanoTime()
        gfgDB.clearAndInsertTestData()
        pauseOffset += java.lang.System.nanoTime() - gfgStarter
        gfgDB.complexQuery()
    }
    Log.d("Geeks", "databaseGeeks_offset: $pauseOffset")
}

但是,此代码存在问题。如果我们不更改数据库中的值,我们的查询可以被缓存,我们将得到与预期不同的结果。因此,我们可以将 db.clearAndInsertTestData() 放入循环中,通过在每个循环中这样做,我们可以破坏缓存,然后测量我们感兴趣的代码。然而,因为我们在循环中还有一个语句,所以我们测量的超出了要求的范围。为了消除这个,我们可以使用前面的方法,即计算或找到 db.clearAndInsertTestData() 所花费的时间,然后从最终输出中减去这个结果。

Android Studio 集成

由于 Benchmark Library 仍处于 alpha 阶段,我们必须下载 Android Studio 3.5 或更高版本。在我们的项目中,我们喜欢模块化,默认情况下,我们有一个 app 模块和一个 lib 模块。要使用 Benchmark 库,您必须先启用 Benchmark 模块,然后再启用 Android Studio Benchmarking 模板。

在 Android Studio 中启用基准模块的步骤如下:

  1. 安装最新版本的 Android Studio(3.5 或更高版本)。
  2. 在 Android Studio 中单击帮助 > 编辑自定义属性。
  3. 插入以下代码:npw.benchmark.template.module=true
  4. 保存并退出文件。
  5. 重新启动 Android Studio。

除了 app 和 lib 模块,我们现在还可以创建 Benchmark 模块。基准模块模板将自动配置基准设置。要制作 Benchmark 模块,请执行以下步骤:

配置为基准

因为我们正在为 Benchmark 创建一个模块,所以我们只有一个 build.gradle 文件。基准模块是相同的。 Benchmark 的 build.gradle 文件包含以下几行:

  1. 基准测试插件:当您运行 gradlew 时,它将帮助您检索基准报告。
  2. 定制运行器:自定义运行器,例如 AndroidbenchmarkRunner,将有助于稳定您的基准。
  3. 预编程的 proguard 规则:这使您的代码更有效率。
  4. 独立的库(alpha 版本,截至目前)

科特林

apply plugin: 'com.android.library'
apply plugin: 'androidx.benchmark'
  
android {
    defaultConfig {
        testInstrumentationRunner "androidx.benchmark.junit4.AndroidBenchmarkRunner"
    }
  
    buildTypes {
        debug {
            debuggable true
            minifyEnabled true
            proguardFiles getDefaultProguardFile(
                    'proguard-android-optimize.txt'),
                    'benchmark-proguard-rules.pro'
        }
    }
}
}

所以我们已经完成了基准测试。但这里有一个问题:我们能否在所有情况下都使用 Jetpack Benchmark Library,即该库是否在所有情况下都产生相同的结果?不,不是的。但别担心;如果有东西干扰了基准,图书馆会通知你。是的,基准库中有一些警告。所以,下次你的 debuggable 设置为 true,或者你正在使用模拟器,或者你缺少跑步者,或者你的电池电量不足时,基准测试会给你一个警告,你可以看看并在基准测试之前对其进行修复。

基准影响因素

执行基准测试任务并不像看起来那么简单。它有很多敌人,其中最危险的是 CPU 时钟。 CPU 时钟对系统稳定性至关重要。更具体地说,CPU时钟问题分为两个问题,即

  • 斜坡
  • 节流

我们都知道,当设备处于理想状态时,即没有分配工作,时钟为低电平,当分配给它一个任务时,时钟开始斜升到高性能模式。因此,在负载下,时钟通常会增加。因此,如果我们在这两种情况下进行测量,我们很可能会因为时钟不同而得到错误的代码测量输出。

这个问题的解决方案很简单,它包含在基准测试的定义中。基准库执行预热以稳定这一点,这意味着基准测试循环将在测量完成之前运行至少 250 毫秒。因此,如果时钟

节流或潜水

当您的设备努力工作时,它会变热并且时钟会迅速下降。通常,当设备发热时,CPU 会降低时钟以保护芯片。热节流是另一个术语。因此,这会对基准测试性能产生重大影响,因为时钟可能前一时刻非常高,而下一时刻又非常低。热节流(红线)的示例如下:

科特林

// AndroidBenchmarkRunner.kt
override fun onCreate(arguments: Bundle) {
    super.onCreate(arguments)
  
    if(sustainedPerformanceModeInUse) {
        gfgMainThread(name = "BenchSpinGfgMainThread") {
            Process.setGfgMainThreadPriority(Process.GFGMAINTHREAD_PRIORITY_LOWEST)
            while(true) {}
        }
    }
}

对于单线程和多线程问题,我们必须强制设备进入多线程模式,因为您只能在最大时钟下使用一个内核或在较低时钟下使用多个内核。但是,切换模式会导致不一致。因此,我们应该强制设备以多线程模式运行。为了完成同样的事情,当我们的 AndroidBenchmarkRunner 处于持续性能模式时,我们会创建一个新线程来旋转。这是强制多线程模式最有效的方法。到目前为止,我们已经看到了两种方法:

  • 时钟锁定
  • 持续偏好

但是,这两种方法都不能在正常情况下使用,因为并非所有设备都已root,并且并非所有设备都支持持续性能模式。我们需要一个具体而广泛的解决方案。所以,节流的最终解决方案是线程休眠,这也是最简单的解决方案。

我们通过在每个基准测试之间运行一个微型基准测试来检测 Thread.sleep() 方法的减速,以查看设备是否已经开始热节流。如果我们检测到热节流,我们会丢弃当前的基准数据并进入睡眠状态以让设备冷却下来。

科特林

@get:Rule
val gfgRule = aBenchmarkRule()
@Test
fun someBench() {
    val gfgFile = File(path)
    benchmarkRule.measureRepeated {
        gfgFile.exists()
    }
}

结论

我们已经看到基准测试是一个非常复杂的问题,并且很难正常测量代码性能。它由时钟稳定性决定。因此,为了解决这些问题,我们创建了 Jetpack Benchmark Library API,它可以为您测量代码性能。另外,请记住,您不应将设备与 JetPack Benchmark 进行比较。在这种情况下,我们正在比较为相同设备和操作系统版本编写的代码。 Benchmark 可以在 Benchmark 网站上找到,基准代码示例可以在 Github 页面上找到。