(两百四十七)Android 学习性能与功耗(一)

学习https://developer.android.google.cn/topic/performance

 

性能与功耗

实现奇特的创意为构建用户满意的应用开了个好头,但这仅仅是个开始。下一步需要尽可能提高应用的性能。例如,用户对应用具有如下的期望:

  • 耗电少。
  • 启动快。
  • 对用户互动响应迅速。

本部分为您提供必要的操作方法,帮助您打造既奇特又高效的应用。请阅读下文,了解如何开发耗电少、响应快、效率高且运行稳定的应用。

 

部分唤醒锁定卡住

部分唤醒锁定是 PowerManager API 中的一种机制,可让开发者在设备的显示屏关闭后(无论是由于系统超时还是用户按下电源按钮)继续保持 CPU 运行。您的应用会通过调用带有 PARTIAL_WAKE_LOCK 标记的 acquire() 来获取部分唤醒锁定。当您的应用在后台运行时,如果部分唤醒锁定保持了较长时间,则会变为卡住状态(用户看不到应用的任何部分)。这种情况会耗尽设备的电量,因为它会阻止设备进入低功耗状态。部分唤醒锁定仅应在必要时使用,并且在不再需要时立即释放。

一般做法

确保您的代码会释放其获取的所有唤醒锁定。这比确保对 acquire() 的每次调用都有对 release() 的对应调用要更复杂。以下是一个由于未捕获的异常而导致未释放唤醒锁定的示例:

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

以下是正确的代码版本:

        void doSomethingAndRelease() throws MyException {
            try {
                wakeLock.acquire();
                doSomethingThatThrows();
            } finally {
                wakeLock.release();
            }
        }
  • 确保唤醒锁定在不再需要时立即释放。例如,如果您使用唤醒锁定来促使后台任务完成,请确保在任务完成后进行释放。如果唤醒锁定的保持时间超出预期而没有释放,则可能意味着后台任务花费的时间超出预期。

锁释放需要放到finally里,不然可能会被之前的异常抛出给跳过。

修复代码中的问题后,请使用以下 Android 工具验证您的应用是否正确释放唤醒锁定:

  • dumpsys - 该工具提供有关设备上系统服务状态的信息。要查看包含唤醒锁定列表的电源服务状态,请运行 adb shell dumpsys power

最佳做法

一般来说,您的应用应避免部分唤醒锁定,因为它太容易耗尽用户的电量。Android 为以前需要部分唤醒锁定的几乎所有用例提供备用 API。部分唤醒锁定的一个剩余用例是确保在屏幕关闭时继续播放音乐应用。如果您使用唤醒锁定来运行任务,请考虑后台处理指南中介绍的替代方法。

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

  • 确保应用的某个部分保留在前台。例如,如果您需要运行服务,则改为启动前台服务。这会直观地向用户表明您的应用仍在运行。
  • 确保获取和释放唤醒锁定的逻辑尽可能简单。当唤醒锁定逻辑与复杂的状态机、超时、执行器池和/或回调事件关联时,该逻辑中的任何细微错误都可能导致唤醒锁定的保持时间超出预期。这些错误很难诊断和调试。

 

过多唤醒

唤醒是 AlarmManager API 中的一种机制,可让开发者设置闹钟以在指定时间唤醒设备。为设置唤醒闹钟,您的应用会调用 AlarmManager 中某个带有 RTC_WAKEUPELAPSED_REALTIME_WAKEUP 标记的 set() 方法。当唤醒闹钟触发时,设备会在执行闹钟的 onReceive()onAlarm() 方法期间退出低功耗模式并保持部分唤醒锁定。如果唤醒闹钟触发次数过多,则可能会耗尽设备的电池电量

 

修复问题

AlarmManager 是在 Android 平台的早期版本中引入的,但随着时间的推移,许多以前需要 的用例现在都改用像 WorkManager 这样的新功能且效果更好。本部分包含有关减少唤醒闹钟的提示,但从长远来看,请考虑迁移您的应用,以遵循最佳做法部分中的建议。

确定您在应用中的哪些位置调度了唤醒闹钟,并降低这些闹钟的触发频率。请参考以下提示:

  • 查找对 AlarmManager 中各种包含 RTC_WAKEUPELAPSED_REALTIME_WAKEUP 标记的 set() 方法的调用。

  • 我们建议您在闹钟的标记名称中包含您的软件包、类或方法名称,以便在源代码中轻松识别设置了闹钟的位置。下面提供了更多相关提示:

    • 在名称中省去任何个人身份信息 (PII),例如电子邮件地址。否则,设备将记录 _UNKNOWN 而不是闹钟名称。
    • 请勿以编程方式(例如通过调用 getName())获取类或方法名称,因为它可能会被 Proguard 混淆。取而代之,应使用硬编码字符串。
    • 请勿向闹钟标签添加计数器或唯一标识符。系统将无法汇总以这种方式设置的闹钟,因为它们都具有唯一标识符。

解决该问题后,运行以下 ADB 命令,验证唤醒闹钟是否正常运行:

adb shell dumpsys alarm
    

此命令提供有关设备上的闹钟系统服务状态的信息。如需了解详情,请参阅 dumpsys

最佳做法

仅当您的应用需要执行面向用户的操作时(例如发布通知或提醒用户),才应使用唤醒闹钟。有关 AlarmManager 最佳做法的列表,请参阅调度重复闹钟

请勿使用 AlarmManager 调度后台任务,特别是重复或网络后台任务。应使用 WorkManager 来调度后台任务,因为它具有以下优势:

  • 批处理 - 将作业合并在一起,以减少耗电量
  • 持久性 - 如果重新启动设备,则调度的 WorkManager 作业会在重新启动后运行
  • 条件 - 作业可以根据条件(例如设备是否正在充电或 WLAN 是否可用)运行

如需了解详情,请参阅后台处理指南

请勿使用 AlarmManager 来调度仅在应用运行期间有效的定时操作(换句话说,当用户退出应用时应取消定时操作)。在这种情况下,请使用 Handler 类,因为它更易于使用且效率高很多。

 

后台 WLAN 扫描次数过多

当应用在后台执行 WLAN 扫描时,它会唤醒 CPU,从而加快耗电速度。扫描次数过多时,设备的电池续航时间可能会明显缩短。如果某个应用处于 PROCESS_STATE_BACKGROUNDPROCESS_STATE_CACHED 状态,则会被视为在后台运行。

后台扫描天然限定30min一次,应该还好,系统的pno扫描是20s~60s

 Android O的扫面场景可以归结为以下四种:

1、 亮屏情况下,在Wifi settings界面,固定扫描,时间间隔为10s。

2、 亮屏情况下,非Wifi settings界面,二进制指数退避扫描,退避:interval*(2^n), 最小间隔min=20s, 最大间隔max=160s.
3、 灭屏情况下,有保存网络时,若已连接,不扫描,否则,PNO扫描,即只扫描已保存的网络。最小间隔min=20s,最大间隔max=20s*3=60s
4、 无保存网络情况下,固定扫描,间隔为5分钟,用于通知用户周围存在可用开放网络。
其中场景1/2/4中的扫描是全信道扫描,扫描控制逻辑在Android framework,涉及模块依次是WifiTracker、WifiConnectivityManager、WifiStateMachine。场景3中的扫描是PNO扫描,即只扫描已保存的网络,PNO扫描的控制逻辑涉及较广,从上到下(Qcom平台):WifiConnectivityManager、WifiScanner、WifiScanningServiceImpl、WifiNative、wificond、wifi driver、firmware。
————————————————
版权声明:本文为CSDN博主「weixin_38503885」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_38503885/article/details/82182169

减少扫描次数

如果可能,您的应用执行 WLAN 扫描时应该是在前台运行。前台服务会自动显示通知;在前台执行 WLAN 扫描,从而让用户知道设备上发生 WLAN 扫描的原因和时间。

如需了解如何在位于前台时进行扫描,请参阅 WifiManager 类的文档。

如果您的应用无法避免在后台运行期间执行 WLAN 扫描,则可能适合采用偷懒至上策略。“偷懒至上”包含三种可用于消减 WLAN 扫描次数的方法:“减少”、“推迟”和“合并”。如需了解这些方法,请参阅针对电池续航时间进行优化

 

后台移动网络使用量过高

当应用在后台连接移动网络时,应用会唤醒 CPU 并开启无线装置。如果反复执行此操作,可能会耗尽设备的电池电量。如果某个应用处于 PROCESS_STATE_BACKGROUNDPROCESS_STATE_CACHED 状态,则会被视为在后台运行。

减少移动网络使用量

您可以将应用的移动网络使用量移至前台,提醒用户目前正在进行下载,并为他们提供暂停或停止下载的控件。为此,请调用 DownloadManager 并根据情况设置 setNotificationVisibility(int)

 

ANR

如果 Android 应用的界面线程处于阻塞状态的时间过长,会触发“应用无响应”(ANR) 错误。如果应用位于前台,系统会向用户显示一个对话框,如图 1 所示。ANR 对话框会为用户提供强行退出应用的选项。

图 1. 向用户显示的 ANR 对话框

图 1. 向用户显示的 ANR 对话框

ANR 是一个问题,因为负责更新界面的应用主线程无法处理用户输入事件或绘制操作,引起用户的不满。如需详细了解应用的主线程,请参阅进程和线程

出现以下任何情况时,系统都会针对您的应用触发 ANR:

  • 当您的 Activity 位于前台时,您的应用在 5 秒钟内未响应输入事件或BroadcastReceiver(如按键或屏幕轻触事件)。
  • 虽然前台没有 Activity,但您的 BroadcastReceiver 用了相当长的时间仍未执行完毕。

诊断 ANR

诊断 ANR 时需要考虑以下几种常见模式:

  1. 应用在主线程上非常缓慢地执行涉及 I/O 的操作。
  2. 应用在主线程上进行长时间的计算。
  3. 主线程在对另一个进程进行同步 binder 调用,而后者需要很长时间才能返回。
  4. 主线程处于阻塞状态,为发生在另一个线程上的长操作等待同步的块。
  5. 主线程在进程中或通过 binder 调用与另一个线程之间发生死锁。主线程不只是在等待长操作执行完毕,而且处于死锁状态。如需更多信息,请参阅维基百科上的死锁

以下方法可帮助您找出是以上哪种原因造成了 ANR。

严格模式

使用 StrictMode 有助于您在开发应用时发现主线程上的意外 I/O 操作。您可以在应用级别或 Activity 级别使用 。

启用后台 ANR 对话框

只有在设备的开发者选项中启用了显示所有 ANR 时,Android 才会针对花费过长时间处理广播消息的应用显示 ANR 对话框。因此,系统并不会始终向用户显示后台 ANR 对话框,但应用仍可能会遇到性能问题。

TraceView

您可以使用 TraceView 在查看用例时获取正在运行的应用的跟踪信息,并找出主线程繁忙的位置。如需了解如何使用 TraceView,请参阅使用 TraceView 和 dmtracedump 分析性能

现在用Android studio的profile了。

拉取跟踪信息文件

Android 会在遇到 ANR 时存储跟踪信息。在较低的操作系统版本中,设备上只有一个 /data/anr/traces.txt 文件。在较新的操作系统版本中,有多个 /data/anr/anr_* 文件。您可以使用 Android 调试桥 (ADB) 作为根,从设备或模拟器中获取 ANR 跟踪信息:

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

您可以使用设备上的“生成错误报告”开发者选项或开发机器上的 adb bugreport 命令,从实体设备获取错误报告。如需了解详情,请参阅获取和阅读错误报告

 

主线程上执行速度缓慢的代码

在您的代码中找出应用的主线程忙碌时间超过 5 秒的位置。在您的应用中查找可疑用例并尝试重现 ANR。

例如,图 2 显示的 TraceView 时间轴中,主线程的忙碌时间超过了 5 秒。

图 2. TraceView 时间轴中显示了一个忙碌的主线程

图 2. TraceView 时间轴中显示了一个忙碌的主线程

图 2 显示了大多数违规代码发生在 onClick(View) 处理程序中,如以下代码示例所示:

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

在这种情况下,您应该将主线程中运行的工作移至工作线程。Android Framework 中包含有助于将任务移至工作线程的类,如需了解详情,请参阅用于线程处理的辅助类。以下代码显示了如何使用 AsyncTask 辅助类处理工作线程上的任务:

    @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);
    }
    

TraceView 显示大部分代码都在工作线程上运行,如图 3 所示。主线程有空闲来响应用户事件。

图 3. TraceView 时间轴中显示了由工作线程处理的工作

图 3. TraceView 时间轴中显示了由工作线程处理的工作

主线程上的 IO

在主线程上执行 IO 操作是导致主线程上操作速度缓慢的常见原因,主线程上操作速度缓慢会导致 ANR。建议将所有 IO 操作移至工作线程,如上一部分所示。

IO 操作示例包括网络和存储操作。如需了解详情,请参阅执行网络操作保存数据

锁争用

在某些情况下,导致 ANR 的工作并不是直接在应用的主线程上执行。如果某工作线程持有对某项资源的锁,而该资源是主线程完成其工作所必需的,这种情况下就可能会发生 ANR。

例如,图 4 显示的 TraceView 时间轴中,大部分工作是在工作线程上执行的。

图 4. TraceView 时间轴中显示了工作线程上正在执行的工作

图 4. TraceView 时间轴中显示了工作线程上正在执行的工作

但如果用户仍然会遇到 ANR,您应该在 Android Device Monitor 中查看主线程的状态。通常情况下,如果主线程已准备好更新界面并且总体上响应速度较快,则处于 RUNNABLE 状态。

但如果主线程无法继续执行,则它处于 BLOCKED 状态,并且无法响应事件。该状态在 Android Device Monitor 中会显示为“Monitor”或“Wait”,如图 5 所示。

图 5. 处于“Monitor”状态的主线程

图 5. 处于“Monitor”状态的主线程

以下跟踪信息显示了因等待资源而处于阻塞状态的应用主线程:

...
    AsyncTask #2" prio=5 tid=18 Runnable
      | group="main" sCount=0 dsCount=0 obj=0x12c333a0 self=0x94c87100
      | sysTid=25287 nice=10 cgrp=default sched=0/0 handle=0x94b80920
      | state=R schedstat=( 0 0 0 ) utm=757 stm=0 core=3 HZ=100
      | stack=0x94a7e000-0x94a80000 stackSize=1038KB
      | held mutexes= "mutator lock"(shared held)
      at com.android.developer.anrsample.BubbleSort.sort(BubbleSort.java:8)
      at com.android.developer.anrsample.MainActivity$LockTask.doInBackground(MainActivity.java:147)
      - locked <0x083105ee> (a java.lang.Boolean)
      at com.android.developer.anrsample.MainActivity$LockTask.doInBackground(MainActivity.java:135)
      at android.os.AsyncTask$2.call(AsyncTask.java:305)
      at java.util.concurrent.FutureTask.run(FutureTask.java:237)
      at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:243)
      at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
      at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
      at java.lang.Thread.run(Thread.java:761)
    ...
    

查看跟踪信息有助于找到阻塞主线程的代码。以下代码持有的锁导致了前面的跟踪信息中的主线程阻塞:

    @Override
    public void onClick(View v) {
        // The worker thread holds a lock on lockedResource
       new LockTask().execute(data);

       synchronized (lockedResource) {
           // The main thread requires lockedResource here
           // but it has to wait until LockTask finishes using it.
       }
    }

    public class LockTask extends AsyncTask<Integer[], Integer, Long> {
       @Override
       protected Long doInBackground(Integer[]... params) {
           synchronized (lockedResource) {
               // This is a long-running operation, which makes
               // the lock last for a long time
               BubbleSort.sort(params[0]);
           }
       }
    }
 

另一个示例是应用的主线程在等待来自某工作线程的结果,如以下代码所示。请注意,不建议在 Kotlin 中使用 wait()notify(),Kotlin 有自己的并发操作处理机制。使用 Kotlin 时,应尽量使用 Kotlin 专用机制。

KotlinJava

    public void onClick(View v) {
       WaitTask waitTask = new WaitTask();
       synchronized (waitTask) {
           try {
               waitTask.execute(data);
               // Wait for this worker thread’s notification
               waitTask.wait();
           } catch (InterruptedException e) {}
       }
    }

    class WaitTask extends AsyncTask<Integer[], Integer, Long> {
       @Override
       protected Long doInBackground(Integer[]... params) {
           synchronized (this) {
               BubbleSort.sort(params[0]);
               // Finished, notify the main thread
               notify();
           }
       }
    }
    

还有一些其他情况会阻塞主线程,包括使用 LockSemaphore 以及资源池(如数据库连接池)或其他互斥(互斥锁)机制的线程。

您应总体上评估应用对资源持有的锁,但如果您想避免 ANR,则应查看对主线程所需资源持有的锁。

请确保将持有锁的时间降到最少,或者最好从一开始就评估应用是否需要持有锁。如果您使用锁来确定何时根据工作线程的处理情况来更新界面,请使用 onProgressUpdate()onPostExecute() 之类的机制在工作线程和主线程之间进行通信。

死锁

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

计算机科学领域对死锁现象进行了充分研究,目前有一些死锁预防算法可用于避免死锁。

如需了解详情,请参阅维基百科上的死锁死锁预防算法

执行速度缓慢的广播接收器

应用可以通过广播接收器响应广播消息,例如启用或停用飞行模式或更改连接状态。如果应用处理广播消息的用时过长,就会发生 ANR。

以下情况下会发生 ANR:

您的应用应只在 BroadcastReceiveronReceive() 方法中执行短操作。不过,如果您的应用因广播消息而需要进行更复杂的处理,则应将该任务推迟到 IntentService

您可以使用 TraceView 等工具来识别广播接收器是否在应用的主线程上执行长时间运行的操作。例如,图 6 显示了某广播接收器的时间轴,该接收器在主线程上处理消息用时大约 100 秒。

图 6. TraceView 时间轴中显示了主线程上的 BroadcastReceiver 工作

图 6. TraceView 时间轴中显示了主线程上的 BroadcastReceiver 工作

如果对 BroadcastReceiveronReceive() 方法执行长时间运行的操作,可能会导致此行为,如以下示例所示:

KotlinJava

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

在此类情况下,我们建议将长时间运行的操作移至 IntentService,因为它使用工作线程来执行其工作。以下代码显示了如何使用 IntentService 处理长时间运行的操作:

KotlinJava

    @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);
       }
    }
    

由于使用 IntentService,长时间运行的操作将在工作线程(而非主线程)上执行。图 7 在 TraceView 时间轴中显示了推迟到工作线程的工作。

图 7. TraceView 时间轴中显示了在工作线程上处理的广播消息

图 7. TraceView 时间轴中显示了在工作线程上处理的广播消息

 

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

KotlinJava

    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);
    

不过,如果广播在后台运行,则将代码从执行速度缓慢的广播接收器移至另一个线程并使用 goAsync() 将无法解决 ANR 问题。ANR 超时仍然适用。

 

总结

提升功耗的原因可能是

  • PowerManager部分唤醒锁定
  • AlarmManager唤醒
  • 后台wlan扫描过多(原生30min一次的限制个人觉得还好)
  • 后台移动网络使用过多

 

  • ANR
  • 自己在执行耗时操作,比如io、复杂操作
  • 自己在等其他的资源,比如binder、锁、死锁、阻塞操作

一般是通过trace看下主线程的执行情况来确定问题

anr可以通过新建线程来分担主线程工作,IntentService其实是新建线程一样的导致,service不行,是执行在主线程的。

 

 

 

相关推荐
©️2020 CSDN 皮肤主题: 鲸 设计师:meimeiellie 返回首页