Android性能优化-Android vitals

写在前面

本文不涉及Android vitals工具的使用,仅是介绍相关检测项目,摘录其中对于影响性能的代码的诊断和避免/解决方式并补充一些方法,如有错误,望指出~

2019.04.21

All other vitals:
Excessive background Wi-Fi scans
Excessive background network usage
App startup time
Slow rendering
Frozen frames
Permission denials

2019.04.20

Android vitals - Core Vitals部分
ANR rates
Crash rates
Excessive wakeups
Stuck partial wake locks


官方网站

Android vitals

Android vitals是Google为提高Android设备稳定性和性能而采取的一项举措。当选择加入的用户运行您的应用时,他们的Android设备会记录各种指标,包括有关应用稳定性,应用启动时间,电池使用情况,渲染时间和权限拒绝的数据。 Google Play控制台会聚合此数据并将其显示在Android vitals信息中心中。
dashboard突出显示崩溃率,ANR率,过度唤醒和卡住唤醒锁:这些是开发人员应该关注的核心vital。所有其他vital(如适用于您的应用程序或游戏类型)应受到监控,以确保它们不会产生负面影响。在虚拟环境中展示不良行为会对您的应用中的用户体验产生负面影响,并可能导致Play商店的评级不佳和可发现性差。

目录
Diagnose and fix bad behaviors
Core vitals:

ANR rates
Crash rates
Excessive wakeups
Stuck partial wake locks

All other vitals:

Excessive background Wi-Fi scans
Excessive background network usage
App startup time
Slow rendering
Frozen frames
Permission denials


ANRs

当UI线程被阻塞的时间过长时,会触发“Application Not Responding" (ANR) ”。如果App在前台,系统会为用户显示一个Dialog,ANR dialog可以让用户强制退出app。
在这里插入图片描述

ANRs造成的问题是-因为app的主线程是用来更新UI的,所以当程序无法响应时,app无法及时处理用户输入事件和UI的绘制,会给用户带来困扰。更多关于主线程的问题,请参照 Processes and threads 。

以下情况发生时会使你的app触发ANR:

1.当你的Activity处于前台,且你的app没有在5s内对一个输入事件或广播接受(例如按键或touch事件)进行响应
2.前台并没有Activity,但是你的BoradcastReceiver没有在相当长的时间内完成执行

Diagnosing ANRs

Android提供了多种方法让您知道您的应用存在问题,并帮助您进行诊断。如果您已经发布了应用程序,Android vitals可以提醒您问题正在发生,并且有一些诊断工具可以帮助您找到问题。

有一些常见的诊断ANRs的模式:

1.app在主线程执行包含I/O的慢速操作
2.app在主线程执行长时间的计算
3.主线程中执行同步的binder call(跨进程通信),且另一个进程需要很长时间才能返回数据
4.主线程因为等待同步块而被阻塞,且持有锁的线程在执行长时间的操作
5.主线程与另一个线程发生死锁:在当前进程中或是跨进程操作。主线程并不仅是等待耗时操作,且处于死锁状态

以下技术可以帮助你找到ANRs的原因属于上述的哪一种:

Strict mode

StrictMode最常用于捕获应用程序主线程上的意外磁盘或网络访问,主线程是负责接收UI操作并进行动画。保持磁盘和网络操作不在主线程上,可以使应用程序更加顺畅,响应更快。通过保持应用程序的主线程响应,您还可以防止向用户显示ANR对话框。

Note:即使Android设备的磁盘通常位于闪存上,但许多设备在该内存之上运行文件系统的并发性非常有限。通常情况下,几乎所有磁盘访问都很快,但在某些情况下,当某些I / O在后台发生其他进程时,可能会显着变慢。如果可能的话,最好假设这样的事情并不快。

使用方式:
在Application、Activity或是其他应用组件的onCreate之前

public void onCreate() {
     if (DEVELOPER_MODE) {
         StrictMode.setThreadPolicy(new ThreadPolicy.Builder()
                 .detectDiskReads()
                 .detectDiskWrites()
                 .detectNetwork()   // or .detectAll() for all detectable problems
                 .penaltyLog()
                 .build());
         StrictMode.setVmPolicy(new VmPolicy.Builder()
                 .detectLeakedSqlLiteObjects()
                 .detectLeakedClosableObjects()
                 .penaltyLog()
                 .penaltyDeath()
                 .build());
     }
     super.onCreate();
 }

您可以决定在检测到违规时应该发生什么。例如,使用StrictMode.ThreadPolicy.Builder.penaltyLog()可以在使用应用程序查看违规情况时查看adb logcat的输出。

如果您发现了您认为有问题的违规行为,可以使用各种工具来解决这些问题:threads,Handler,AsyncTask,IntentService等。但是不要觉得有必要修复StrictMode找到的所有内容。特别是,在正常的活动生命周期中,通常需要许多磁盘访问的情况。使用StrictMode查找您意外执行的操作。但是,UI线程上的网络请求几乎总是报出问题。

具体的Api使用请点击查看

Enable background ANR dialogs

Android可以为花费过长事件处理广播消息的情况弹出ANR dialog,但是需要在Developer options中打开Show all ANRs。后台ANR dialog不会总是展示给用户,但是程序依然会遇到性能问题。

Traceview(Deprecated in Android Studio 3.2 or later)

你可以使用Traceview来跟踪正在运行的app,并获取主线程处理的位置。Traceview在Android Studio 3.2 or later已经被废弃了,使用CPU Profiler来替代。

CPU Profiler

CPU Profiler 可帮助您实时检查应用的 CPU 使用率和线程 Activity,并记录函数跟踪,以便您可以优化和调试您的应用代码。

要打开 CPU Profiler,请按以下步骤操作:

1.点击 View > Tool Windows > Android Profiler(也可以点击工具栏中的 Android Profiler )。
2.从 Android Profiler 工具栏中选择您想要分析的设备和应用进程。 如果您通过 USB 连接了某个设备但该设备未在设备列表中列出,请确保您已启用 USB 调试。
3.点击 CPU 时间线中的任意位置即可打开 CPU Profiler。

为什么要分析 CPU 使用率?

最大限度减少应用的 CPU 使用率具有许多优势,如提供更快更顺畅的用户体验,以及延长设备电池续航时间。 它还可帮助应用在各种新旧设备上保持良好性能。 与应用交互时,您可以使用 CPU Profiler 监控 CPU 使用率和线程 Activity。 不过,如需了解应用如何执行其代码的详细信息,您应记录和检查函数跟踪。

对于应用进程中的每个线程,您可以查看一段时间内执行了哪些函数,以及在其执行期间每个函数消耗的 CPU 资源。 您还可以使用函数跟踪来识别调用方和被调用方。 调用方指调用其他函数的函数,而被调用方是指被其他函数调用的函数。 您可以使用此信息确定哪些函数负责调用常常会消耗大量特定资源的任务,并尝试优化应用代码以避免不必要的工作。

如果您想收集可帮助您检查原生系统进程的详细系统级数据,并解决掉帧引起的界面卡顿,您应使用 systrace;或者,如果您想导出您使用 Debug 类捕获的 .trace 文件,您应使用 Traceview。

具体的Api使用请点击查看,其中使用图标清晰的根据你的需求进行函数跟踪:

在这里插入图片描述
Call Chart 标签提供函数跟踪的图形表示形式,其中,水平轴表示函数调用(或调用方)的时间段和时间,并沿垂直轴显示其被调用者。 对系统 API 的函数调用显示为橙色,对应用自有函数的调用显示为绿色,对第三方 API(包括 Java 语言 API)的函数调用显示为蓝色。

Flame Chart 标签提供一个倒置的调用图表,其汇总相同的调用堆栈。 即,收集共享相同调用方顺序的完全相同的函数,并在火焰图中用一个较长的横条表示它们(而不是将它们显示为多个较短的横条,如调用图表中所示)。 这样更方便您查看哪些函数消耗最多时间。 不过,这也意味着水平轴不再代表时间线,相反,它表示每个函数相对的执行时间。

为帮助说明此概念,请考虑以下图中的调用图表。 请注意,函数 D 多次调用 B(B1、B2 和 B3),其中一些对 B 的调用也调用了 C(C1 和 C3)

在这里插入图片描述

由于 B1、B2 和 B3 共享相同的调用方顺序 (A → D → B),因此,可将它们汇总在一起,如下所示。 同样,将 C1 和 C3 汇总在一起,因为它们也共享相同的调用方顺序 (A → D → B → C)—请注意,未包含 C2,因为它具有不同的调用方顺序 (A → D → C)。

在这里插入图片描述

汇总的函数调用用于创建火焰图,如图所示。请注意,对于火焰图中任何给定的函数调用,消耗最多 CPU 时间的被调用方首先显示。

在这里插入图片描述

Pull a traces file

Android在遇到ANR时存储跟踪信息。在较旧的OS版本中,设备上有一个/data/anr/traces.txt文件。在较新的OS版本中,有多个/ data / anr / anr_ *文件。您可以使用Android Debug Bridge(adb)以root身份从设备或模拟器访问ANR跟踪:

adb root
adb shell ls /data/anr
adb pull /data/anr/< filename >

您可以使用设备上的Take bug report开发人员选项或开发计算机上的adb bugreport命令从物理设备捕获bugreport。bugreport包含设备日志,堆栈跟踪和其他诊断信息,可帮助您查找和修复应用中的错误。您可以使用设备上的开发者选项中的Take bug report或开发计算机上的adb bugreport命令从设备捕获错误报告。

开发者选项中的Take bug report

在这里插入图片描述

adb

$ adb bugreport E:\Reports\MyBugReports

Fix the problems
Slow code on the main thread

确定app主线程忙碌超过5s的代码。很多耗时操作都是在onClick中触发的,例如:

@Override
public void onClick(View view) {
    // This task runs on the main thread.
    BubbleSort.sort(data);
}

这种情况下,应该将代码放到工作线程中去执行,这样主线程就可以继续响应其他事件:

@Override
public void onClick(View view) {
   // The long-running operation is run on a worker thread
   new AsyncTask<Integer[], Integer, Long>() {
       @Override
       protected Long doInBackground(Integer[]... params) {
           BubbleSort.sort(params[0]);
       }
   }.execute(data);
}
IO on the main thread

在主线程上执行IO操作是导致主线程运行缓慢的常见原因,这可能导致ANR, 建议将所有IO操作移动到工作线程。

IO操作的一些示例是网络和存储操作。 有关更多信息,请参阅 Performing network operationsSaving data.

Lock contention

有时主线程无法响应事件是因为工作线程持有了主线程要获取的锁,使得主线程等待时间过长而产生ANR。

通常,如果主线程已准备好更新UI并且通常是响应的,则主线程处于RUNNABLE状态。
但是如果主线程无法恢复执行,那么它处于BLOCKED状态并且无法响应事件。 状态在Android设备监视器上显示为Monitor或Wait,如图5所示。

如果要避免ANR,那么您应该查看为主线程所需的资源所持有的锁定。

使用锁之前,应该先评估应用程序是否首先需要持有锁,再确保锁保持最少的时间。 如果您使用锁来确定何时根据工作线程的处理更新UI,请使用onProgressUpdate()和onPostExecute()等机制在工作线程和主线程之间进行通信。

Deadlocks

当线程进入等待状态时发生死锁,因为另一个线程持有所需的资源,该线程也在等待第一个线程持有的资源。 如果应用程序的主线程处于这种情况,ANR可能会发生。

死锁是计算机科学中一个经过充分研究的现象,并且可以使用死锁预防算法来避免死锁。

避免死锁的几种方式:
1.设置加锁顺序
死锁发生在多个线程需要相同的锁,但是获得不同的顺序。
假如一个线程需要锁,那么他必须按照一定得顺序获得锁。
例如加锁顺序是A->B->C,现在想要线程C想要获取锁,那么他必须等到线程A和线程B获取锁之后才能轮到他获取。(排队执行,获取锁)
缺点:按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁,并知道他们之间获取锁的顺序是什么样的。
2.设置加锁时限
在获取锁的时候尝试加一个获取锁的时限,超过时限不需要再获取锁,放弃操作(对锁的请求。)。
若一个线程在一定的时间里没有成功的获取到锁,则会进行回退并释放之前获取到的锁,然后等待一段时间后进行重试。在这段等待时间中其他线程有机会尝试获取相同的锁,这样就能保证在没有获取锁的时候继续执行比的事情。
缺点:但是由于存在锁的超时,通过设置时限并不能确定出现了死锁,每种方法总是有缺陷的。有时为了执行某个任务。某个线程花了很长的时间去执行任务,如果在其他线程看来,可能这个时间已经超过了等待的时限,可能出现了死锁。
在大量线程去操作相同的资源的时候,这个情况又是一个不可避免的事情,比如说,现在只有两个线程,一个线程执行的时候,超过了等待的时间,下一个线程会尝试获取相同的锁,避免出现死锁。但是这时候不是两个线程了,可能是几百个线程同时去执行,大的基数让事件出现的概率变大,假如线程还是等待那么长时间,但是多个线程的等待时间就有可能重叠,因此又会出现竞争超时,由于他们的超时发生时间正好赶在了一起,而超时等待的时间又是一致的,那么他们下一次又会竞争,等待,这就又出现了死锁。
3.死锁检测
当一个线程获取锁的时候,会在相应的数据结构中记录下来,相同下,如果有线程请求锁,也会在相应的结构中记录下来。当一个线程请求失败时,需要遍历一下这个数据结构检查是否有死锁产生。
例如:线程A请求锁住一个方法1,但是现在这个方法是线程B所有的,这时候线程A可以检查一下线程B是否已经请求了线程A当前所持有的锁,像是一个环,线程A拥有锁1,请求锁2,线程B拥有锁2,请求锁1。
当遍历这个存储结构的时候,如果发现了死锁,一个可行的办法就是释放所有的锁,回退,并且等待一段时间后再次尝试。
缺点:这个这个方法和上面的超时重试的策略是一样的。但是在大量线程的时候问题还是会出现和设置加锁时限相同的问题。每次线程之间发生竞争。
还有一种解决方法是设置线程优先级,这样其中几个线程回退,其余的线程继续保持着他们获取的锁,也可以尝试随机设置优先级,这样保证线程的执行。

Slow broadcast receivers

应用可以通过广播接收器响应广播消息,例如启用或禁用飞行模式或改变连接状态。 当应用程序花费太长时间来处理广播消息时会发生ANR。

ANR在以下情况下发生:

1.广播接收器长时间内未完成onReceive()方法。
2.广播接收器调用goAsync(),但无法在PendingResult对象上调用finish()。
您的应用只应在BroadcastReceiver的onReceive()方法中执行简短操作。 但是,如果您的应用程序因广播消息而需要更复杂的处理,则应将任务推迟到IntentService。

您可以使用Traceview等工具来识别您的广播接收器是否在应用程序的主线程上执行长时间运行的操作。

@Override
public void onReceive(Context context, Intent intent) {
    // This is a long-running operation
    BubbleSort.sort(data);
}

这种情况下,建议将耗时操作放入IntentService中去执行,因为IntentService使用的是工作线程。

@Override
public void onReceive(Context context, Intent intent) {
    // The task now runs on a worker thread.
    Intent intentService = new Intent(context, MyIntentService.class);
    context.startService(intentService);
}

public class MyIntentService extends IntentService {
   @Override
   protected void onHandleIntent(@Nullable Intent intent) {
       BubbleSort.sort(data);
   }
}

您的广播接收器可以使用goAsync()向系统发出信号,表明它需要更多时间来处理消息。 但是,您应该在PendingResult对象上调用finish()。 以下示例显示如何调用finish()以使系统回收广播接收器并避免ANR:

// 这种方式,当遇到广播处于后台运行时,并不能消除ANR
final PendingResult pendingResult = goAsync();
new AsyncTask<Integer[], Integer, Long>() {
   @Override
   protected Long doInBackground(Integer[]... params) {
       // This is a long-running operation
       BubbleSort.sort(params[0]);
       pendingResult.finish();
   }
}.execute(data);

Crashes

每当出现由未处理的exception或signal引起的意外退出时,Android应用程序就会崩溃。 使用Java编写的应用程序如果抛出未处理的exception(由Throwable类表示)则会崩溃。 如果在执行期间存在未处理的signal(例如SIGSEGV),则使用本机代码语言编写的应用程序会崩溃。
应用程序无需在前台运行也可以崩溃。 任何应用程序组件,甚至是在后台运行的广播接收器或内容提供商等组件都可能导致应用程序崩溃。 这些崩溃通常会让用户感到困惑,因为他们没有对应用进行任何操作。

Diagnose the crashes
Reading a stack trace

修复崩溃的第一步是确定崩溃发生的位置。 如果使用Play Console或logcat工具的输出,则可以使用报告详细信息中提供的堆栈跟踪。 如果您没有可用的堆栈跟踪,则应通过手动测试应用程序或联系受影响的用户来本地重现崩溃,并在使用logcat时重现它。

堆栈跟踪显示两条对调试崩溃至关重要的信息:

1.抛出的异常类型
2.引发异常的代码部分

AndroidRuntime: FATAL EXCEPTION: main
Process: com.android.developer.crashsample, PID: 3686
java.lang.NullPointerException: crash sample
	at com.android.developer.crashsample.MainActivity$1.onClick(MainActivity.java:27)
	at android.view.View.performClick(View.java:6134)
	at android.view.View$PerformClick.run(View.java:23965)
	at android.os.Handler.handleCallback(Handler.java:751)
	at android.os.Handler.dispatchMessage(Handler.java:95)
	at android.os.Looper.loop(Looper.java:156)
	at android.app.ActivityThread.main(ActivityThread.java:6440)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:746)

关于Native发生的Crash请查看Native crash诊断

Tips for reproducing a crash

Memory errors

如果您有OutOfMemoryError,那么您可以创建一个内存容量较低的模拟器。 您可以在AVD管理器设置中控制设备上的内存量。

Networking exceptions

由于用户经常移入和移出移动或WiFi网络覆盖范围,因此在应用程序网络中,异常通常不应被视为错误,而是作为意外发生的正常操作条件。

如果您需要重现网络异常,例如UnknownHostException,请尝试在应用程序尝试使用网络时启用飞行模式。
另一种选择是通过选择网络速度模拟和/或网络延迟来降低模拟器中网络的质量。 您可以使用AVD管理器上的“速度”和“延迟”设置,也可以使用-netdelay和-netspeed标志启动模拟器,如以下命令行示例所示:

// delay of 20 seconds on all network requests and an upload and download speed of 14.4 Kbps
emulator -avd [your-avd-image] -netdelay 20000 -netspeed gsm

Excessive wakeups

唤醒是AlarmManager API中的一种机制,允许开发人员设置警报以在指定时间唤醒设备。 您的应用程序通过使用RTC_WAKEUP或ELAPSED_REALTIME_WAKEUP标志调用AlarmManager中的一个set()方法来设置唤醒警报。 当触发唤醒警报时,设备退出低功耗模式并在执行警报的onReceive()或onAlarm()方法时保持部分唤醒锁定。 如果过度触发唤醒警报,则可以耗尽设备的电池电量。

Fix the problem

AlarmManager是在早期版本的Android平台中引入的,但随着时间的推移,许多以前需要使用AlarmManager的用例现在可以通过WorkManager等新功能得到更好的服务。

确定应用程序中您安排唤醒警报的位置,并降低触发警报的频率。 以下是一些提示:

1.查找对AlarmManager中包含RTC_WAKEUP或ELAPSED_REALTIME_WAKEUP标志的各种set()方法的调用。
2.我们建议您在警报的标签名称中包含您的package,class或method名称,以便您可以轻松识别源中设置警报的位置。 以下是一些其他提示:
(1)在名称中留下任何个人识别信息(PII),例如电子邮件地址。 否则,设备会记录_UNKNOWN而不是警报名称。
(2)不要以编程方式获取类或方法名称,例如通过调用getName(),因为它可能会被Proguard混淆。 而是使用硬编码的字符串。
(3)请勿向警报标签添加计数器或唯一标识符。 系统将无法聚合以这种方式设置的警报,因为它们都具有唯一标识符。

通过运行以下ADB命令验证唤醒警报是否按预期工作:

adb shell dumpsys alarm

Best practices

仅当您的应用需要执行面向用户的操作(例如发布通知或警告用户)时才使用唤醒警报。不要使用AlarmManager来安排后台任务,尤其是重复或网络后台任务。使用WorkManager计划后台任务,因为它具有以下优点:

1.batching 批处理 - 将工作结合起来,以减少电池消耗
2.persistence 持久性 - 即使在重新启动设备后,标记为持久的作业也将继续运行
3.criteria 条件 - 作业可以根据条件运行,例如设备是否正在充电或WiFi是否可用

不要使用AlarmManager来安排仅在应用程序运行时有效的计时操作(换句话说,当用户退出应用程序时应取消计时操作)。在这些情况下,请使用Handler类,因为它更易于使用且效率更高。


Stuck partial wake locks

部分唤醒锁是PowerManager API中的一种机制,它允许开发人员在设备显示关闭后保持CPU运行(无论是由于系统超时还是用户按下电源按钮)。 您的应用通过使用PARTIAL_WAKE_LOCK标志调用acquire()获取部分唤醒锁定。 如果应用程序在后台运行时,部分唤醒锁定会被卡住(用户看不到应用程序的任何部分)。 这种情况会耗尽设备的电池电量,因为它会阻止设备进入低功耗状态。 部分唤醒锁应仅在必要时使用,并在不再需要时立即释放。

Fix the problem

在早期版本的Android平台中引入了唤醒锁,但随着时间的推移,许多以前需要唤醒锁的用例现在可以通过更新的API(如WorkManager)得到更好的服务。

识别并修复代码中获取唤醒锁定的位置,例如newWakeLock(int,String)或WakefulBroadcastReceiver。 以下是一些提示:

1.确保您的代码释放它获取的所有唤醒锁定。 这比确保每次调用acquire()都有相应的release()调用要复杂得多。 这是一个由于未捕获的异常而未释放的唤醒锁示例:

void doSomethingAndRelease() throws MyException {
        wakeLock.acquire();
        doSomethingThatThrows();
        wakeLock.release();  // does not run if an exception is thrown
    }

// a correct version of the code
void doSomethingAndRelease() throws MyException {
        try {
            wakeLock.acquire();
            doSomethingThatThrows();
        } finally {
            wakeLock.release();
        }
    }

2.确保唤醒锁在不再需要时立即释放。 例如,如果您使用唤醒锁定以允许后台任务完成,请确保在该任务完成时发生释放。 如果唤醒锁定的持续时间超过预期而未被释放,则可能意味着您的后台任务所花费的时间超出预期。

3.同上文excessive wakeups中的建议2

修改后,使用一下工具来确认:
1.dumpsys - a tool that provides information about the status of system services on a device. To see the status of the power service, which includes a list of wake locks, run adb shell dumpsys power.
2.Battery Historian - a tool that parses the output of an Android bug report into a visual representation of power related events.

Best practices

通常,您的应用应该避免部分唤醒锁定,因为它太容易耗尽用户的电池。 Android为几乎所有以前需要部分唤醒锁定的用例提供了替代API。 部分唤醒锁定的一个剩余用例是确保在屏幕关闭时继续播放音乐应用程序。 如果您使用唤醒锁来运行任务,请考虑background processing guide中描述的替代方法。

如果必须使用部分唤醒锁,请遵循以下建议:

1.确保您应用的某些部分仍然在前台。 例如,如果您需要运行服务,请改为启动前台服务。 这可以直观地向用户显示您的应用仍在运行。
2.确保获取和释放唤醒锁的逻辑尽可能简单。 当唤醒锁逻辑与复杂状态机,超时,执行程序池和/或回调事件相关联时,该逻辑中的任何细微错误都可能导致唤醒锁保持的时间超过预期。 这些错误很难诊断和调试。


Excessive Wi-Fi Scanning in the Background

当应用程序在后台执行Wi-Fi扫描时,它会唤醒CPU,从而导致电池耗尽率。 当扫描次数过多时,设备的电池寿命可能会明显缩短。 如果应用程序处于PROCESS_STATE_BACKGROUND或PROCESS_STATE_CACHED状态,则认为该应用程序在后台运行。

Investigate the Wi-Fi scans

Battery Historian等工具可以帮助您更深入地了解应用的扫描行为。 Battery Historian基于每个应用程序提供Wi-Fi扫描行为的可视化,可帮助您更清楚地了解应用程序的运行情况。 有关Battery Historian的更多信息,请参阅使用Battery Historian分析电源使用情况

Reduce the scans

如果可能,您的应用应该在应用程序在前台运行时执行Wi-Fi扫描。 前台服务自动提供通知; 在前台执行Wi-Fi扫描,从而使用户了解Wi-Fi扫描在其设备上发生的原因和时间。

如果您的应用无法避免在应用在后台运行时执行Wi-Fi扫描,则可能会因应用Lazy First策略而受益。 Lazy First包含三种可用于减少Wi-Fi扫描的技术:减少(reduce),推迟(defer)和合并(coalesce)。

优化电池寿命

在保持应用节能的过程中,要记住三件重要的事情:

1.让你的应用程序Lazy First。
2.利用可帮助管理应用程序电池消耗的平台功能。
3.使用可以帮助您识别电池耗尽的原因的工具。

1.Lazy First

Lazy First意味着寻找减少和优化特别是电池密集型操作的方法。 支持Lazy First设计的核心问题是:

Reduce:你的应用程序可以删除冗余操作吗? 例如,它是否可以缓存下载的数据而不是反复唤醒radio以重新下载数据?
Defer:应用程序是否需要立即执行操作? 例如,它可以等到设备充电才能将数据备份到云端吗?
Coalesce:可以批处理工作,而不是多次将设备置于活动状态吗? 例如,几十个应用程序是否真的有必要在不同时间打开收音机发送邮件? 在一次唤醒收音机期间,是否可以传输消息?

2.Platform features

从广义上讲,Android平台提供两类帮助,帮助您优化应用的电池使用。 首先,它提供了几个可以在您的应用中实现的API。 您可以在Intelligent Job Scheduling中了解有关这些API的更多信息。

平台中还有内部机制来帮助延长电池寿命。 虽然它们不是您以编程方式实现的API,但您仍应了解它们,以便您的应用程序可以成功利用它们。 有关更多信息,请参阅:

1.Doze and App Standby
2.App Standby Buckets.系统根据用户的使用模式限制应用程序对CPU或电池等设备资源的访问
3.Background restrictions.如果某个应用出现不良行为,系统会提示用户限制该应用对系统资源的访问权限
4.Power management restrictions.
5.Testing and troubleshooting

此外,Android 9(API级别28)对节电模式进行了许多改进。 设备制造商确定所施加的精确限制。 例如,在AOSP构建中,系统应用以下限制:

系统更加积极地将应用程序置于应用程序待机模式,而不是等待应用程序空闲。
后台执行限制适用于所有应用程序,无论其目标API级别如何。
屏幕关闭时可能会禁用位置服务。
后台应用程序没有网络访问权限。

请参阅电源管理限制中特定于设备的电源优化的完整详细信息。

可以在打开省电模式时测试应用,通过设备的“设置”>“省电模式”手动打开省电模式。

Tooling

有适用于Android的工具,包括Profile GPU RenderingBattery Historian,可帮助您识别可以优化的区域,从而延长电池寿命。 利用这些工具来定位可以应用Lazy First原则的区域。


Excessive Mobile Network Usage in Background

简介同上。

Investigating mobile-network-usage behavior

使用工具同上。

Reduce mobile network usage

由于应用正在执行同步,因此可能会出现移动网络使用情况。您可以将应用程序的移动网络使用情况移至前台,提醒用户下载正在进行中,并为他们提供暂停或停止下载的控件。 为此,请调用DownloadManager,并根据需要设置setNotificationVisibility(int)。


App startup time

用户希望应用程序能够快速响应并快速加载。 启动时间较慢的应用程序无法达到此预期,并且可能会令用户失望。 这种糟糕的体验可能会导致用户在Play商店中对您的应用评分不佳,甚至完全放弃您的应用。

本文档提供的信息可帮助您优化应用的发布时间。 它首先解释了启动过程的内部结构。 接下来,它将讨论如何分析启动性能。 最后,它描述了一些常见的启动时间问题,并提供了一些如何解决它们的提示。

Understand app-start internals

应用程序启动可以在三种状态之一中进行,每种状态都会影响应用程序对用户可见所需的时间:冷启动,热启动或热启动。 在冷启动时,您的应用程序从头开始。 在其他状态下,系统需要将正在运行的应用程序从后台运行到前台。 我们建议您始终根据冷启动的假设进行优化。 这样做也可以改善热启动和热启动的性能。

为了优化您的应用程序以便快速启动,了解系统和应用程序级别发生的情况以及它们在每种状态下的交互方式非常有用。

Cold start

冷启动是指应用程序从头开始:系统的进程在此开始之前没有创建应用程序的进程。 冷启动发生在诸如自设备启动以来首次启动应用程序或自系统终止应用程序以来。 这种类型的启动在最小化启动时间方面提出了最大的挑战,因为系统和应用程序比其他启动状态有更多的工作要做。

在冷启动开始时,系统有三个任务。 这些任务是:

1.加载并启动应用程序。
2.启动后立即显示应用程序的空白启动窗口。
3.创建应用程序进程。

一旦系统创建了应用程序流程,应用程序流程就会负责下一个阶段:

1.创建应用程序对象。
2.启动主线程。
3.创建主要活动。
4.Inflating views。
5.布局屏幕。
6.执行初始draw。

应用程序进程完成第一次绘制后,系统进程会交换当前显示的背景窗口,将其替换为MainActivity。 此时,用户可以开始使用该应用程序。

在这里插入图片描述

Application creation

当您的应用程序启动时,空白的启动窗口将保留在屏幕上,直到系统首次完成绘制应用程序。 此时,系统进程会交换应用程序的启动窗口,允许用户开始与应用程序进行交互。

如果您在自己的应用程序中重载了Application.onCreate(),系统会在您的应用程序对象上调用onCreate()方法。 之后,应用程序生成主线程(也称为UI线程),并通过创建主要活动来执行任务。

Activity creation

应用程序进程创建活动后,活动将执行以下操作:

1.Initializes values。
2.调用构造函数。
3.调用适合于活动当前生命周期状态的回调方法,例如Activity.onCreate()。

通常,onCreate()方法对加载时间的影响最大,因为它以最高的开销执行工作:加载和inflating views,以及初始化Activity运行所需的对象。

Hot start

应用程序的热启动比冷启动更简单,开销更低。 在一个hot start中,所有系统都会将您的活动带到前台。 如果您的所有应用程序的活动仍然驻留在内存中,那么应用程序可以避免重复对象初始化,layout inflation和渲染。

但是,如果为了响应内存修整事件(例如onTrimMemory())而清除了某些内存,则需要重新创建这些对象以响应热启动事件。

热启动显示与冷启动方案相同的屏幕行为:系统进程显示空白屏幕,直到应用程序完成呈现活动。

Warm start

热启动包括冷启动期间发生的一些操作子集; 同时,它比热启动更少的开销。 有许多潜在的状态可以被视为热启动。 例如:

1.用户退出您的应用,但随后重新启动它。 该过程可能已继续运行,但应用程序必须通过调用onCreate()从头开始重新创建活动。
2.系统将您的应用程序从内存中逐出,然后用户重新启动它。 需要重新启动进程和活动,但是任务可以从传递给onCreate()的saved instance state bundle中获益。

Diagnose problems
Diagnosing slow startup times

为了正确诊断开始时间性能,您可以跟踪指示应用程序启动所需时间的指标。

Time to initial display

在Android 4.4(API级别19)及更高版本中,logcat包含一个包含名为Displayed的值的输出行。 此值表示启动过程和完成在屏幕上绘制相应活动之间所经过的时间量。 经过的时间包括以下事件序列:

1.启动进程。
2.初始化对象。
3.创建并初始化Activity。
4.inflate布局。
5.首次绘制application。

报告的日志行类似于以下示例:

ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms

如果您正在从logcat或terminal中跟踪logcat输出,则查找已用时间非常简单。 要在Android Studio中查找已用时间,必须在logcat视图中禁用Filters。 禁用Filters是必要的,因为系统服务器而不是应用程序本身为此日志提供服务。

在这里插入图片描述
logcat输出中的Displayed度量标准不一定捕获加载和显示所有资源之前的时间量:它会遗漏布局文件中未引用的资源或应用程序在对象初始化过程中创建的资源。 它排除了这些资源,因为加载它们是一个内联过程,并不会阻止应用程序的初始显示。

有时,logcat输出中的Displayed行包含总时间的附加字段。 例如:

ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms (total +1m22s643ms)

在这种情况下,第一次测量仅适用于首次绘制的活动。 总时间测量从应用程序进程开始时开始,可能包括另一个首先启动但未向屏幕显示任何内容的活动。 仅在单个活动与总启动时间之间存在差异时才显示总时间测量值。

您还可以使用ADB Shell Activity Manager命令运行应用程序来测量初始显示的时间。 例如:

adb [-d|-e|-s ] shell am start -S -W
com.example.app/.MainActivity
-c android.intent.category.LAUNCHER
-a android.intent.action.MAIN
-c和-a参数是可选的,允许您为intent指定和。

显示的度量标准与以前一样出现在logcat输出中。 您的终端窗口还应显示以下内容:

Starting: Intent
Activity: com.example.app/.MainActivity
ThisTime: 2044
TotalTime: 2044
WaitTime: 2054
Complete

Time to full display

您可以使用reportFullyDrawn()方法来度量应用程序启动和完整显示所有资源和视图层次结构之间所用的时间。在应用程序执行延迟加载的情况下,这可能很有用。在延迟加载中,应用程序不会阻止窗口的初始绘制,而是异步加载资源并更新视图层次结构。

如果由于延迟加载,应用程序的初始显示不包括所有资源,您可能会将所有资源和视图的已完成加载和显示视为单独的度量标准:例如,您的UI可能已完全加载,并绘制了一些文本,但尚未显示应用必须从网络中获取的图像。

要解决此问题,您可以手动调用reportFullyDrawn(),让系统知道您的活动已完成其延迟加载。使用此方法时,logcat显示的值是从创建应用程序对象到调用reportFullyDrawn()的时间。这是logcat输出的一个例子:

system_process I/ActivityManager: Fully drawn {package}/.MainActivity: +1s54ms

Be aware of common issues

本节讨论通常会影响应用程序启动性能的几个问题。 这些问题主要涉及初始化应用程序和活动对象,以及加载屏幕。

Heavy app initialization

当代码覆盖Application对象时,启动性能会受到影响,并且在初始化该对象时执行繁重的工作或复杂的逻辑。 如果您的应用程序子类执行不需要执行的初始化,则您的应用程序可能会在启动期间浪费时间。 某些初始化可能完全没有必要:例如,初始化主活动的状态信息,当应用实际启动以响应意图时。 根据意图,应用程序仅使用先前初始化的状态数据的子集。

应用程序初始化期间的其他的影响事件包括有影响或数量众多的垃圾收集事件,或者磁盘I / O与初始化同时发生,进一步阻止了初始化过程。 垃圾收集是Dalvik runtime的一个重要考虑因素; Art runtime并行执行垃圾收集,最大限度地减少操作的影响。

可以使用Method tracing或Inline tracing来尝试诊断问题。

Method tracing

运行CPU Profiler会发现callApplicationOnCreate()方法最终会调用com.example.customApplication.onCreate方法。 如果该工具显示这些方法需要很长时间才能完成执行,那么您应该进一步探索以查看正在进行的工作。

Inline tracing

使用内联跟踪来调查可能的原因,包括:

1.您应用的初始onCreate()函数。
2.您的应用初始化的任何全局单例对象。
3.在bottleneck期间可能发生的任何磁盘I / O,反序列化或tight循环。

Solutions to the problem

无论问题在于不必要的初始化还是磁盘I / O,解决方案都会调用惰性初始化对象:仅初始化那些立即需要的对象。 例如,不是创建全局静态对象,而是移动到单例模式,其中应用程序仅在第一次访问对象时初始化对象。 此外,考虑使用像Dagger这样的依赖注入框架来创建对象,并且它们是第一次注入时的依赖关系。

Heavy activity initialization

创建活动通常需要大量高额开销。 通常,有机会优化这项工作以实现性能改进。 这些常见问题包括:

1.inflate大型或复杂的布局。
2.阻止磁盘上的屏幕绘制或网络I / O.
3.加载和解码位图。
4.栅格化VectorDrawable对象。
5.初始化acrtivity的其他子系统。

诊断问题方法同上。

Solutions to the problem

存在许多潜在的瓶颈,但两个常见问题和补救措施如下:

1.视图层次结构越大,应用程序对其进行inflate的时间就越长。 您可以采取的两个步骤来解决此问题:
(1)通过减少冗余或嵌套布局来展平视图层次结构。
(2)不会在启动期间不需要显示UI的部分内容。 相反,使用ViewStub对象作为子层次结构的占位符,应用程序可以在更合适的时间膨胀。
2.在主线程上进行所有资源初始化也会降低启动速度。 您可以按如下方式解决此问题:
(1)移除所有资源初始化,以便应用程序可以在另一个线程上懒加载它。
(2)允许应用加载并显示您的视图,然后更新依赖于位图和其他资源的可视属性。

Themed launch screens

您可能希望为应用程序的加载体验添加theme,以便应用程序的启动屏幕在主题上与应用程序的其余部分保持一致,而不是系统主题。 这样做可以隐藏缓慢的活动启动。

实现主题启动屏幕的常用方法是实现一个启动屏,使用windowDisablePreview属性关闭系统进程在启动应用程序时绘制的初始空白屏幕。 但是,与不抑制预览窗口的应用程序相比,此方法可能会导致启动时间更长。 此外,它会强制用户在活动启动时等待,并且没有反馈,会让用户怀疑应用程序是否正常运行。

Solutions to the problem

我们建议您不要禁用预览窗口,而是遵循常见的Material Design模式。 您可以使用activity的windowBackground主题属性为起始活动提供简单的自定义drawable。

例如,您可以创建一个新的drawable文件,并从布局XML和应用程序清单文件中引用它,如下所示:

// Layout XML file:
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
  <!-- The background color, preferably the same as your normal theme -->
  <item android:drawable="@android:color/white"/>
  <!-- Your product logo - 144dp color version of your app icon -->
  <item>
    <bitmap
      android:src="@drawable/product_logo_144dp"
      android:gravity="center"/>
  </item>
</layer-list>

// Manifest file:
<activity ...
android:theme="@style/AppTheme.Launcher" />

转换回普通主题的最简单方法是在调用super.onCreate()和setContentView()之前调用setTheme(R.style.AppTheme):

public class MyMainActivity extends AppCompatActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // Make sure this is before calling super.onCreate
    setTheme(R.style.Theme_MyApp);
    super.onCreate(savedInstanceState);
    // ...
  }
}

Slow rendering

UI渲染是从应用程序生成框架并在屏幕上显示框架的行为。 为了确保用户与您的应用程序的交互顺利,您的应用程序应该在16毫秒内渲染帧以达到每秒60帧(为什么60fps?)。 如果您的应用程序遭受慢速UI渲染,那么系统将被迫跳过一些帧,用户将感知应用程序卡顿。 我们称之为jank。

Identifying jank

精确定位应用程序中导致jank的代码可能很困难。 本节介绍了识别jank的三种方法:

1.Visual inspection(可视化检查)
2.Systrace
3.Custom performance monitoring

可视化检查使您可以在几分钟内快速浏览应用程序中的所有用例,但它不能提供与Systrace一样多的详细信息。 Systrace提供了更多详细信息,但如果您针对应用程序中的所有用例运行Systrace,那么您将收到很多的数据以至于难以分析。 视觉检查和systrace都会检测到您本地设备上的抖动。 如果您的jank无法在本地设备上重现,您可以构建自定义性能监视,以在现场运行的设备上测量应用程序的特定部分。

With visual inspection

目视检查可帮助您识别产生jank的用例。要执行visual inspection,请打开您的应用程序并手动浏览应用程序的不同部分,并注意难以实现的UI。以下是执行目视检查时的一些提示:

1.运行应用程序的发行版(或至少不可调试版)。 ART运行时禁用了几个重要的优化以支持调试功能,因此请确保您正在查看类似于用户将看到的内容。
2.启用Profile GPU Rendering。Profile GPU Rendering在屏幕上显示条形图,使您可以快速直观地显示相对于每帧16毫秒基准测试渲染UI窗口的帧所需的时间。每个条形图都有着色组件,这些组件映射到渲染管道中的一个舞台,因此您可以看到哪个部分花费的时间最长。例如,如果框架花费大量时间处理输入,您应该查看处理用户输入的应用程序代码。
3.某些组件,例如RecyclerView,是jank的常见来源。如果您的应用使用这些组件,则最好运行应用的这些部分
4.有时,只有当应用程序从冷启动启动时才可以复制jank。
5.尝试在较慢的设备上运行您的应用程序以加剧问题。

With Systrace

虽然Systrace是一个显示整个设备正在运行的工具,但它可用于识别应用中的jank。 Systrace具有最小的系统开销,因此您将在仪器仪表期间体验到逼真的jankiness。
在您的设备上执行janky用例时,使用Systrace记录跟踪。 有关如何使用Systrace的说明,请参阅Systrace演练。 systrace由进程和线程分解。 在Systrace中查找应用程序的过程,如图。

在这里插入图片描述

1.Systrace显示每个帧的绘制时间,并对每个帧进行颜色编码以突出显示较慢的渲染时间。 这有助于您比视觉检查更准确地找到单个janky框架。
2.Systrace可检测应用程序中的问题,并在单个框架和警报面板中显示alert。 alert中的指示是您的最佳选择。
3.Android框架和库的一部分(例如RecyclerView)包含跟踪标记。 因此,systrace时间轴显示何时在UI线程上执行这些方法以及执行它们需要多长时间。

查看systrace输出后,您的应用程序中可能存在您怀疑导致jank的方法。 例如,如果时间线显示由RecyclerView花费很长时间导致慢速帧,则可以将Trace标记添加到相关代码并重新运行systrace以获取更多信息。 在新的systrace中,时间轴将显示调用应用程序方法的时间以及执行时间。

如果systrace没有向您显示有关UI线程长时间工作的详细信息,那么您将需要使用Android CPU Profiler来记录采样或检测方法跟踪。 一般来说,方法跟踪不利于识别jank,因为它们由于开销很大而产生false-positive jank,并且它们无法看到线程运行时阻塞。 但是,方法跟踪可以帮助您确定应用中的方法占用的时间最多。 确定这些方法后,添加Trace标记并重新运行systrace以查看这些方法是否导致jank。

注意:记录systrace时,每个跟踪标记(执行的开始和结束对)会增加大约10μs的开销。 为避免误报janks,请不要在一帧中调用数十次或短于200us的方法中添加跟踪标记。

With custom performance monitoring

如果您无法在本地设备上重现jank,则可以在应用程序中构建自定义性能监视,以帮助识别现场设备上的jank来源。

为此,使用FrameMetricsAggregator从应用程序的特定部分收集帧渲染时间,并使用Firebase性能监视记录和分析数据。

Fixing jank

要修复jank,请检查哪些帧未在16.7ms内完成,并查找出现了什么问题。 记录视图#绘制在某些帧中是否异常长,或者可能是布局? 有关这些问题,请参阅下面的常见jank来源和其他问题。

为了避免jank,长时间运行的任务应该在UI线程之外异步运行。 始终要知道您运行的代码是什么线程,并在向主线程发布非平凡任务时要小心。

如果您的应用程序具有复杂且重要的主要UI(可能是滚动列表),请考虑编写可自动检测缓慢渲染时间并经常运行测试以防止回归的检测测试。

Common sources of jank
Scrollable lists

ListView尤其是RecyclerView通常用于最容易受jank影响的复杂滚动列表。 它们都包含Systrace标记,因此您可以使用Systrace来确定它们是否对您的应用中的jank有贡献。 确保传递命令行参数-a 以获取RecyclerView中的跟踪部分(以及您添加的任何跟踪标记)以显示。 如果可用,请遵循systrace输出中生成的警报的指导。 在Systrace中,您可以单击RecyclerView跟踪的部分以查看RecyclerView正在执行的工作的说明。

RecyclerView: notifyDataSetChanged

如果您在一个框架中看到RecyclerView中的每个项目都被rebound(并因此重新布局并重新绘制),请确保您没有调用notifyDataSetChanged(),setAdapter(Adapter)或swapAdapter(Adapter,boolean) 用于小更新。 这些方法表示整个列表内容已更改,并将在Systrace中显示为RV FullInvalidate。 相反,使用SortedList或DiffUtil在内容更改或添加时生成最少的更新。

例如,考虑从服务器接收新版本的新闻内容的应用程序。 当您将该信息发布到适配器时,可以调用notifyDataSetChanged(),如下所示:

void onNewDataArrived(List<News> news) {
    myAdapter.setNews(news);
    myAdapter.notifyDataSetChanged();
}

但这有一个很大的缺点 - 如果它是一个微不足道的变化(可能是一个单独的项目添加到顶部),RecyclerView不知道 - 它被告知放弃所有缓存的项目状态,因此需要重新绑定一切。最好使用DiffUtil,它将为您计算和发送最少的更新。

// define your MyCallback as a DiffUtil.Callback implementation to inform DiffUtil how to inspect your lists
void onNewDataArrived(List<News> news) {
    List<News> oldNews = myAdapter.getItems();
    DiffResult result = DiffUtil.calculateDiff(new MyCallback(oldNews, news));
    myAdapter.setNews(news);
    result.dispatchUpdatesTo(myAdapter);
}

RecyclerView: Nested RecyclerViews

嵌套RecyclerViews很常见,尤其是水平滚动列表的垂直列表(如Play商店主页上的应用程序网格)。这可以很好用,但也有很多view在移动。 如果您在第一次向下滚动页面时看到许多inner item在inflate,您可能需要检查是否在内部(水平)RecyclerViews之间共享RecyclerView.RecycledViewPools。 默认情况下,每个RecyclerView都有自己的项目池。 如果同时在屏幕上显示十几个itemViews,则当不同的水平列表无法共享itemView时,如果所有行都显示相似类型的视图,则会出现问题。

class OuterAdapter extends RecyclerView.Adapter<OuterAdapter.ViewHolder> {
    RecyclerView.RecycledViewPool sharedPool = new RecyclerView.RecycledViewPool();

    ...

    @Override
    public void onCreateViewHolder(ViewGroup parent, int viewType) {
        // inflate inner item, find innerRecyclerView by ID…
        LinearLayoutManager innerLLM = new LinearLayoutManager(parent.getContext(),
                LinearLayoutManager.HORIZONTAL);
        innerRv.setLayoutManager(innerLLM);
        innerRv.setRecycledViewPool(sharedPool);
        return new OuterAdapter.ViewHolder(innerRv);

    }

    ...

如果要进一步优化,还可以在内部RecyclerView的LinearLayoutManager上调用setInitialPrefetchItemCount(int)。 例如,如果您连续可见3.5个项目,请调用innerLLM.setInitialItemPrefetchCount(4);. 这将向RecyclerView发出信号,当一个水平行即将到来时,它应该尝试预取内部的项目,如果在UI线程上有空闲时间。

RecyclerView: Too much inflation / Create taking too long

在大多数情况下,通过提前完成工作,而UI线程处于空闲状态,RecyclerView中的预取功能应该有助于解决inflation的成本问题。 如果您在一帧中看到了inflation(而不是标记为RV Prefetch的部分),请确保您在最近的设备上进行测试(目前仅在Android 5.0 API Level 21及更高版本上支持预取)并使用最新版本 支持库。

如果您在屏幕上看到新项目时经常看到inflation导致jank,请确认是否创建了过多的视图类型。 RecyclerView内容中的视图类型越少,当新项目类型出现在屏幕上时,需要进行的inflation就越少。 如果可能,合并视图类型 - 如果只有图标,颜色或文本片段在类型之间发生变化,您可以在绑定时进行更改,并避免inflation(同时减少应用程序的内存占用量)。

如果您的视图类型看起来很好,请考虑降低inflation成本。 减少不必要的容器和结构视图可以提供帮助 - 考虑使用ConstraintLayout构建itemViews,这样可以轻松减少结构视图。 如果你想真正优化性能,你的项目层次结构很简单,你不需要复杂的主题和样式功能,考虑自己调用构造函数 - 请注意,通常不值得权衡失去simplicity和功能XML。

RecyclerView: Bind taking too long

Bind(即onBindViewHolder(VH,int))应该非常简单,除了最复杂的项目之外,其他所有内容都要少于1毫秒。 它应该从适配器的内部项数据中获取POJO项,并在ViewHolder中的视图上调用setter。 如果RV OnBindView需要很长时间,请验证您在绑定代码中的工作量是否正常。

如果您使用简单的POJO对象来保存适配器中的数据,则可以完全避免使用DataBinding在onBindViewHolder中编写绑定代码。

ListView: Inflation

如果你不小心,很容易在ListView中意外禁用回收。 如果您每次在屏幕上显示项目时都会看到inflation,请检查您的Adapter.getView()实现是否正在使用,重新绑定并返回convertView参数。 如果你的getView()实现总是inflate,你的应用程序将无法从ListView中获得回收的好处。 getView()的结构应该几乎总是类似于下面的实现:

View getView(int position, View convertView, ViewGroup parent) {

    if (convertView == null) {
        // only inflate if no convertView passed
        convertView = layoutInflater.inflate(R.layout.my_layout, parent, false)
    }
    // … bind content from position to convertView …
    return convertView;
}
Layout performance

如果Systrace显示Choreographer#doFrame的布局部分做了太多工作,或者经常做工作,那就意味着你遇到了布局性能问题。 应用程序的布局性能取决于View层次结构的哪个部分具有更改布局参数或输入。

Layout performance: Cost

如果segment长度超过几毫秒,则可能会遇到RelativeLayouts或weighted-LinearLayouts的最坏情况嵌套性能。 这些布局中的每一个都可以触发其子项的多个度量/布局过程,因此嵌套它们会导致嵌套深度上的O(n ^ 2)行为。 尝试在除层次结构的最低叶节点之外的所有节点中避免RelativeLayout或LinearLayout的权重特征。 有几种方法可以做到这一点:

1.您可以重新组织结构视图。
2.您可以定义自定义布局逻辑。
3.您可以尝试转换为ConstraintLayout,它提供类似的功能,没有性能缺陷。

Layout performance: Frequency

当新内容出现在屏幕上时,例如当新项目在RecyclerView中滚动到视图中时,预计会发生布局。 如果在每个帧上发生重要布局,则可能是动画布局,这可能会导致帧丢失。 通常,动画应该在View的绘图属性上运行(例如setTranslationX / Y / Z(),setRotation(),setAlpha()等…)。 这些都可以比布局属性(例如填充或边距)更便宜地更改。 更改视图的绘图属性也要便宜得多,通常是通过调用setter来触发invalidate(),然后在下一帧中绘制draw(Canvas)。 这将重新记录无效视图的绘图操作,并且通常也比布局消耗更少。

Rendering performance

Android UI分两个阶段工作 - 在UI线程上记录视图#绘图,在RenderThread上绘制DrawFrame。 第一个在每个无效的View上运行draw(Canvas),并可以调用自定义视图或代码中的调用。 第二个在本机RenderThread上运行,但将根据Record View#draw阶段生成的工作进行操作。

Rendering performance: UI Thread

如果Record View#draw需要很长时间,那么通常会在UI线程上绘制Bitmap。 绘制到Bitmap会使用CPU渲染,因此通常应该避免此操作。 您可以使用Android CPU Profiler进行方法跟踪,以查看是否存在问题。

当应用程序想要在显示Bitmap之前装饰Bitmap时,通常会绘制到Bitmap。 有时像添加圆角的装饰:

Canvas bitmapCanvas = new Canvas(roundedOutputBitmap);
Paint paint = new Paint();
paint.setAntiAlias(true);
// draw a round rect to define shape:
bitmapCanvas.drawRoundRect(0, 0,
        roundedOutputBitmap.getWidth(), roundedOutputBitmap.getHeight(), 20, 20, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
// multiply content on top, to make it rounded
bitmapCanvas.drawBitmap(sourceBitmap, 0, 0, paint);
bitmapCanvas.setBitmap(null);
// now roundedOutputBitmap has sourceBitmap inside, but as a circle

如果这是您在UI线程上所做的工作,则可以在后台的decode线程上执行此操作。 在某些情况下,你甚至可以在draw时完成工作,所以如果你的Drawable或View代码看起来像这样:

void setBitmap(Bitmap bitmap) {
    mBitmap = bitmap;
    invalidate();
}

void onDraw(Canvas canvas) {
    canvas.drawBitmap(mBitmap, null, paint);
}

// 修改为

void setBitmap(Bitmap bitmap) {
    shaderPaint.setShader(
            new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
    invalidate();
}

void onDraw(Canvas canvas) {
    canvas.drawRoundRect(0, 0, width, height, 20, 20, shaderPaint);
}

请注意,这通常也可以用于背景保护(在Bitmap上绘制渐变)和图像过滤(使用ColorMatrixColorFilter),另外两个常见的操作是修改Bitmap。

如果您因为其他原因(可能将其用作缓存)绘制到Bitmap,请尝试直接传递给View或Drawable的硬件加速Canvas,如有必要,请考虑使用LAYER_TYPE_HARDWARE调用setLayerType()来缓存复杂渲染 输出,仍然利用GPU渲染。

Rendering performance: RenderThread

某些canvas操作记录起来很便宜,但会在RenderThread上触发高消耗的计算。 Systrace通常会通过警报调用它们。

Canvas.saveLayer()

避免使用Canvas.saveLayer() - 它可以触发每帧的昂贵,未缓存,屏幕外渲染。 虽然在Android 6.0中性能有所提高(进行了优化,以避免在GPU上进行渲染目标切换时的消耗),但如果可能的话,仍然可以避免使用这种昂贵的API,或者至少确保您传递Canvas.CLIP_TO_LAYER_SAVE_FLAG(或调用不带flag的variant)。

Animating large Paths

当Canvas.drawPath()在传递给Views的硬件加速Canvas上调用时,Android首先在CPU上绘制这些路径,然后将它们上传到GPU。 如果您有大路径,请避免逐帧编辑它们,以便可以高效缓存和绘制它们。 drawPoints(),drawLines()和drawRect / Circle / Oval / RoundRect()效率更高 - 即使最终使用更多绘制调用,最好使用它们。

Canvas.clipPath

clipPath(Path)触发昂贵的剪切行为,通常应该避免。 如果可能,选择绘制形状,而不是剪裁到非矩形。 处理会更好并支持抗锯齿。 例如,以下clipPath调用:

canvas.save();
canvas.clipPath(circlePath);
canvas.drawBitmap(bitmap, 0f, 0f, paint);
canvas.restore();

// 可以替换为:
// one time init:
paint.setShader(new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
// at draw time:
canvas.drawPath(circlePath, mPaint);

Bitmap uploads

Android将位图显示为OpenGL纹理,并且第一次在帧中显示位图时,它会上传到GPU。 你可以在Systrace中看到这个上传宽度x高度纹理。 这可能需要几毫秒,但有必要使用GPU显示图像。

如果这些花费很长时间,请首先检查迹线中的宽度和高度数。 确保显示的位图不会明显大于它显示的屏幕上的区域。如果是,则浪费上传时间和内存。 通常,位图加载库提供了一种简单的方法来请求适当大小的位图。

在Android 7.0中,位图加载代码(通常由库完成)可以调用prepareToDraw()以在需要之前提前触发上载。 这样,上传就会提前发生,而RenderThread则处于空闲状态。 只要能获取到位图,就可以在解码后或将位图绑定到View时完成此操作。 理想情况下,您的位图加载库将为您执行此操作,但如果您自己管理,或者想要确保不在新设备上进行上载,则可以在自己的代码中调用prepareToDraw()。

Thread scheduling delays

线程调度程序是Android操作系统的一部分,负责决定系统中的哪些线程应该运行,何时运行以及运行多长时间。 有时,因为您的应用程序的UI线程被阻止或未运行而出现jank。 Systrace使用不同的颜色来指示线程何时处于休眠状态(灰色),Runnable(蓝色:它可以运行,但调度程序尚未选择它运行),正在运行(绿色)或不可中断 睡觉(红色或橙色)。 这对于调试由线程调度延迟引起的jank问题非常有用。

在这里插入图片描述
注意:一帧的某些时刻不会让UI线程或RenderThread运行。 例如,在RenderThread的syncFrameState运行并且上传位图时,UI线程被阻止 - 这样RenderThread可以安全地复制UI线程使用的数据。 另一个例子,RenderThread在使用IPC时可以阻塞:在帧的开头获取缓冲区,从中查询信息,或者使用eglSwapBuffers将缓冲区传递回合成器。

通常,应用程序执行中的长时间暂停是由于binder请求,Android上的进程间通信(IPC)机制引起的。 在Android的最新版本中,这是UI Thread停止运行的最常见原因之一。 通常,修复方式是避免调用binder请求; 如果它是不可避免的,你应该缓存值,或者将工作移到后台线程。 随着代码库变大,通过调用一些低级方法很容易意外地添加binder请求 - 但是通过跟踪来查找和修复它们同样容易。

如果您有binder transactions,则可以使用以下adb命令捕获其调用堆栈:

$ adb shell am trace-ipc start
… use the app - scroll/animate …
$ adb shell am trace-ipc stop --dump-file /data/local/tmp/ipc-trace.txt
$ adb pull /data/local/tmp/ipc-trace.txt

有时像getRefreshRate()这样无害的看似调用会触发binder transactions,并且在频繁调用它们时会导致大问题。 定期跟踪可以帮助您快速查找并解决这些问题。

在这里插入图片描述
如果您没有看到binder transactions activity,但仍未看到您的UI线程运行,请确保您没有等待来自其他线程的某些锁定或其他操作。 通常,UI线程不应该等待来自其他线程的结果 - 其他线程应该向其发布信息。

Object allocation and garbage collection

自从ART作为Android 5.0中的默认运行时引入ART以来,对象分配和垃圾收集(GC)已成为一个问题,但仍然可以通过这项额外的工作来减轻线程的负担。 可以为不会发生多次(例如用户单击按钮)分配以响应罕见的事件,但请记住每次分配都需要付出代价。 如果它处于频繁调用的紧密循环中,请考虑避免分配以减轻GC上的负载。

Systrace将向您显示GC是否经常运行,并且Android Memory Profiler可以显示分配的来源。 如果你可以避免分配,特别是在紧密循环中,你应该没有问题。

在最新版本的Android上,GC通常在名为HeapTaskDaemon的后台线程上运行。 请注意,大量分配可能意味着在GC上花费的CPU资源更多,如图所示。

在这里插入图片描述

Frozen frames

冻结帧是需要超过700毫秒才能渲染的UI帧。 这是一个问题,因为您的应用程序似乎卡住了,并且在帧渲染时几乎整整一秒都没有响应用户输入。 我们通常建议应用程序在16毫秒内渲染帧以确保流畅的UI。 但是,当您的应用程序启动或转换到其他屏幕时,初始帧的绘制时间超过16毫秒是正常的,因为您的应用必须使inflate views,layout screen并从头开始执行初始绘制。 这就是为什么Android与慢速渲染分开跟踪冻结帧的原因。 您的应用中的任何帧都不应超过700毫秒来渲染。

冻结帧是慢速渲染的极端形式,因此诊断和解决问题的过程是相同的。 有关诊断和修复慢速渲染的信息,请参阅slow rendering。

Note:Android vitals仪表板和Android系统会跟踪使用UI工具包的应用程序的冻结帧(应用程序的用户可见部分是从Canvas或View层次结构中绘制的)。 如果您的应用程序不使用UI工具包,就像使用Vulkan,Unity,Unreal或OpenGL构建的应用程序一样,那么Android Vitals报告中不提供冻结帧和其他渲染时间统计信息。 您可以通过运行adb shell dumpsys gfxinfo 来确定您的设备是否正在记录应用的渲染时间指标。


Permission Denials

大多数应用都要求用户授予他们某些应用权限才能正常运行。 但是,在某些情况下,用户可能不会授予权限

1.他们认为应用程序的核心功能不需要权限。
2.它们不使用与权限关联的功能。
3.他们担心许可会影响设备性能。
4.他们只是不舒服,例如由于对隐私的敏感性。

Best practices

异常高的拒绝率表明,用户不认为他们的信息的额外曝光值得提供回报的好处。 有许多方法可以让用户更轻松地使用您的应用。 如果您采取本节中概述的步骤,您可能可以降低拒绝率。 但是,您不应期望将拒绝率降至零,因为用户具有不同的个人偏好,有些人根本不希望在任何情况下授予权限。

Avoid requesting unnecessary permissions

研究表明,用户更喜欢请求较少权限的应用。 将权限请求保持在必要的最小设置可以帮助提高用户对应用程序的信任,并推动更多安装。 相反,添加不必要的权限请求可能会对您的应用在Play商店中的可见性产生负面影响。 如果不需要特定权限,您可以通过其他方法减少应用程序的权限请求数。 应用程序权限最佳实践中概述了一些常见方法。很棒,荐读,从技术和用户心理两方面详细的介绍了权限获取的方式。

Surface the permission request in context

不太直观的Non-critical权限可能会受益于上下文中的解释。 这样做可以提高用户对权限派生值的理解。 当应用程序在相关功能的上下文中请求权限时,用户可以更好地理解价值主张。 这种改进的理解可能会说服更多用户授予权限请求。
有关如何教育用户和请求权限的良好指导的详细信息,请参阅权限的材料设计模式

Explain why your app needs the permission

考虑从context中请求您的权限开始:提供不太直观的权限的解释有助于提高用户对权限的理解。 [shouldShowRequestPermissionRationale()](/ reference / androidx / core / app / ActivityCompat#shouldShowRequestPermissionRationale(android.app.Activity,java.lang.String)实用程序方法如果用户先前已拒绝该请求,则返回true。您的应用程序可以使用 此方法确定何时显示解释。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值