关于电量性能优化的总结

耗电设备

手机各个硬件模块的耗电量是不同的,有些模块很是耗电,而有些模块则相对显得耗电量小不少。android

电量消耗的计算与统计是一件麻烦并且矛盾的事情,记录电量消耗自己也是一个费电量的事情。惟一可行的方案是使用第三方监测电量的设备,这样才可以获取到真实的电量消耗。git

屏幕

当设备处于待机状态时消耗的电量是极少的,以 Nexus 5 为例,打开飞行模式,能够待机接近 1 个月。但是点亮屏幕,味着系统的各组件要开始进行工做,界面也须要开始执行渲染,这会须要消耗不少电量。github

蜂窝网络

一般状况下,使用移动网络传输数据,电量的消耗有三种状态:web

  • Full Power

能量最高的状态,移动网络链接被激活,容许设备以最大的传输速率进行操做。面试

  • Low power

一种中间状态,对电量的消耗差很少是 Full power 状态下的 50%。docker

  • Standby

最低的状态,没有数据链接须要传输,电量消耗最少。shell

总之,为了减小电量的消耗,在蜂窝移动网络下,最好作到批量执行网络请求,尽可能避免频繁的间隔网络请求。windows

使用 Battery Historian 咱们能够获得设备的电量消耗数据,若是数据中的移动蜂窝网络(Mobile Radio)电量消耗呈现下面的状况,间隔很小,又频繁断断续续的出现,说明电量消耗性能很很差:浏览器

battery bad

通过优化以后,若是呈现下面的图示,说明电量消耗的性能是良好的:缓存

battery good

另外 WiFi 链接下,网络传输的电量消耗要比移动网络少不少,应该尽可能减小移动网络下的数据传输,多在 WiFi 环境下传输数据。

battery wif

那么如何才可以把任务缓存起来,作到批量化执行呢?咱们可使用 JobScheduler 来优化。

跟踪充电状态

咱们能够经过下面的代码来获取手机的当前充电状态:

IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
Intent batteryStatus = this.registerReceiver(null, filter);
int chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
boolean acCharge = (chargePlug == BatteryManager.BATTERY_PLUGGED_AC);
if (acCharge) {
    Log.v(LOG_TAG, "The phone is charging!");
}

在上面的例子演示了如何当即获取到手机的充电状态,获得充电状态信息以后,咱们能够有针对性的对部分代码作优化。

好比:咱们能够判断只有当前手机为 AC 充电状态时 才去执行一些很是耗电的操做。
private boolean checkForPower() {
  IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
  Intent batteryStatus = this.registerReceiver(null, filter);

  int chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
  boolean usbCharge = (chargePlug == BatteryManager.BATTERY_PLUGGED_USB);
  boolean acCharge = (chargePlug == BatteryManager.BATTERY_PLUGGED_AC);
  boolean wirelessCharge = false;
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
    wirelessCharge =
      (chargePlug == BatteryManager.BATTERY_PLUGGED_WIRELESS);
  }
  return (usbCharge || acCharge || wirelessCharge);
}

监听充电状态变化

在清单文件中注册一个 BroadcastReceiver,经过在一个 Intent 过滤器内定义 ACTION_POWER_CONNECTED 和 ACTION_POWER_DISCONNECTED 来同时侦听这两种事件。

<receiver android:name=".PowerConnectionReceiver">
  <intent-filter>
    <action android:name="android.intent.action.ACTION_POWER_CONNECTED"/>
    <action android:name="android.intent.action.ACTION_POWER_DISCONNECTED"/>
  </intent-filter>
</receiver>

建立监听充电状态变化的 PowerConnectionReceiver。

public class PowerConnectionReceiver extends BroadcastReceiver {
  @Override
  public void onReceive(Context context, Intent intent) {
    int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS,
                                   BatteryManager.BATTERY_STATUS_UNKNOWN);
    String batteryStatus = "";
    switch (status) {
      case BatteryManager.BATTERY_STATUS_CHARGING:
        batteryStatus = "正在充电";
        break;
      case BatteryManager.BATTERY_STATUS_DISCHARGING:
        batteryStatus = "正在放电";
        break;
      case BatteryManager.BATTERY_STATUS_NOT_CHARGING:
        batteryStatus = "未充电";
        break;
      case BatteryManager.BATTERY_STATUS_FULL:
        batteryStatus = "充满电";
        break;
      case BatteryManager.BATTERY_STATUS_UNKNOWN:
        batteryStatus = "未知道状态";
        break;
    }
    Toast.makeText(context, "batteryStatus = " + batteryStatus,
                   Toast.LENGTH_LONG).show();
    int plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED,
                                     BatteryManager.BATTERY_PLUGGED_AC);
    String chargePlug = "";
    switch (plugged) {
      case BatteryManager.BATTERY_PLUGGED_AC:
        chargePlug = "AC充电";
        break;
      case BatteryManager.BATTERY_PLUGGED_USB:
        chargePlug = "USB充电";
        break;
      case BatteryManager.BATTERY_PLUGGED_WIRELESS:
        chargePlug = "无线充电";
        break;
    }
    Toast.makeText(context, "chargePlug=" + chargePlug,
                   Toast.LENGTH_LONG).show();
  }
}

最后注册 PowerConnectionReceiver,这时当充电状态发生变化时 PowerConnectionReceiver 就会收到通知。

IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
this.registerReceiver(new PowerConnectionReceiver(), intentFilter);

监听电池电量变化

在清单文件中注册一个 BroadcastReceiver,经过侦听 ACTION_BATTERY_LOW 和 ACTION_BATTERY_OKAY,每当设备电池电量不足或退出不足状态时,便会触发该接收器。

<receiver android:name=".BatteryLevelReceiver">
  <intent-filter>
      <action android:name="android.intent.action.ACTION_BATTERY_LOW"/>
      <action android:name="android.intent.action.ACTION_BATTERY_OKAY"/>
  </intent-filter>
</receiver>

建立监听电池电量变化的 BatteryLevelReceiver。

public class BatteryLevelReceiver extends BroadcastReceiver {
  @Override
  public void onReceive(Context context, Intent intent) {
    // 当前剩余电量
    int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
    // 电量最大值
    int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
    // 电量百分比
    float batteryPct = level / (float) scale;
    Log.d("BatteryLevelReceiver", "batteryPct = " + batteryPct);
    Toast.makeText(context, "batteryPct = " + batteryPct,
                   Toast.LENGTH_LONG).show();
  }
}

最后注册 BatteryLevelReceiver,这时当电池电量发生变化时 BatteryLevelReceiver 就会收到通知。

IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
this.registerReceiver(new BatteryLevelReceiver(), intentFilter);

一般,若是设备链接了交流充电器,您应该最大限度提升后台更新的频率;而若是设备是经过 USB 充电,则应下降更新频率,若是电池正在放电,则应进一步下降更新频率;在电池电量极低时停用全部后台更新。

WakeLock

WakeLock 是一种锁的机制,只要有应用拿着这个锁,CPU 就没法进入休眠状态,一直处于工做状态。

好比,手机屏幕在屏幕关闭的时候,有些应用依然能够唤醒屏幕提示用户消息,这里就是用到了 Wakelock 锁机制,虽然手机屏幕关闭了,可是这些应用依然在运行着。

手机耗电的问题,大部分是开发人员没有正确使用这个锁,成为「待机杀手」。

Android 手机有两个处理器,一个叫 Application Processor(AP),一个叫 Baseband Processor(BP)。

AP 是 ARM 架构的处理器,用于运行 Linux + Android 系统;BP 用于运行实时操做系统(RTOS),通信协议栈运行于 BP 的 RTOS 之上。非通话时间,BP 的能耗基本上在 5mA 左右,而 AP 只要处于非休眠状态,能耗至少在 50mA 以上,执行图形运算时会更高。另外 LCD 工做时功耗在 100mA 左右,WiFi 也在 100mA 左右。

通常手机待机时,AP、LCD、WIFI 均进入休眠状态,这时 Android 中应用程序的代码也会中止执行。

Android 为了确保应用程序中关键代码的正确执行,提供了 Wake Lock 的 API,使得应用程序有权限经过代码阻止 AP 进入休眠状态。但若是不领会 Android 设计者的意图而滥用 Wake Lock API,为了自身程序在后台的正常工做而长时间阻止 AP 进入休眠状态,就会成为待机电池杀手。

那么 Wake Lock API 具体有啥用呢?心跳包从请求到应答,断线重连从新登录等关键逻辑的执行过程,就须要 Wake Lock 来保护。而一旦一个关键逻辑执行成功,就应该当即释放掉 Wake Lock 了。两次心跳请求间隔 5 到 10 分钟,基本不会怎么耗电。

WakeLock 使用

PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
WakeLock wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
                                   "MyWakelockTag");

newWakeLock(int levelAndFlags, String tag) 中 PowerManager.PARTIIAL_WAKE_LOCK 是一个标志位,标志位是用来控制获取的 WakeLock 对象的类型,主要控制 CPU 工做时屏幕是否须要亮着以及键盘灯须要亮着,标志位说明以下:

levelAndFlags CPU是否运行 屏幕是否亮着 键盘灯是否亮着
PARTIAL_WAKE_LOCK
SCREEN_DIM_WAKE_LOCK 低亮度
SCREEN_BRIGHT_WAKE_LOCK 高亮度
FULL_WAKE_LOCK
特殊说明:自 API 等级 17 开始,FULL_WAKE_LOCK 将被弃用。应用应使用 FLAG_KEEP_SCREEN_ON

WakeLock 类能够用来控制设备的工做状态。使用该类中的 acquire 可使 CPU 一直处于工做的状态,若是不须要使 CPU 处于工做状态就调用 release 来关闭。

  • 自动 release

若是咱们调用的是 acquire(long timeout),那么就无需咱们本身手动调用 release() 来释放锁,系统会帮助咱们在 timeout 时间后释放。

  • 手动 release

若是咱们调用的是 acquire() 那么就须要咱们本身手动调用 release() 来释放锁。

最后使用 WakeLock 类记得加上以下权限:

<uses-permission android:name="android.permission.WAKE_LOCK" />
注意:在使用该类的时候,必须保证 acquire 和 release 是成对出现的。

屏幕保持常亮

当设备从休眠状态中,被应用程序唤醒一瞬间会耗电过多,咱们能够保持屏幕常亮来节省电量,代码声明:

// 屏幕保持常亮
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
// 通常不须要人为的去掉 FLAG_KEEP_SCREEN_ON 的 flag,
// windowManager 会管理好程序进入后台回到前台的的操做
//getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

或者,直接在布局中加上 keepScreenOn = true :

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:keepScreenOn="true"
    tools:context="com.jeanboy.app.batterysample.MainActivity">
</android.support.constraint.ConstraintLayout>

JobScheduler

在 API 21,Google 提供了一个新叫作 Job Scheduler API 的组件来处理这样的场景。Job Scheduler API 容许同时执行多个任务,执行某些指定的任务时不须要考虑时机控制引发的电池消耗。

使用 Job Scheduler,应用须要作的事情就是判断哪些任务是不紧急的,能够交给 Job Scheduler 来处理,Job Scheduler 集中处理收到的任务,选择合适的时间,合适的网络,再一块儿进行执行。

下面是使用 Job Scheduler 的一段简要示例,须要先有一个 JobService:

public class MyJobService extends JobService {

  @Override
  public boolean onStartJob(JobParameters params) {
    Log.i("MyJobService", "Totally and completely working on job " 
          + params.getJobId());
    // 检查网络状态
    if (isNetworkConnected()) {
      new SimpleDownloadTask() .execute(params);
      // 返回 true,表示该工做耗时,
      // 同时工做处理完成后须要调用 onStopJob 销毁(jobFinished)
      return true;
    } else {
      Log.i("MyJobService", "No connection on job " + params.getJobId()
            + "; sad face");
    }
    // 返回 false,任务运行不须要很长时间,到 return 时已完成任务处理
    return false;
  }

  @Override
  public boolean onStopJob(JobParameters params) {
    Log.i("MyJobService", "Something changed, so I'm calling it on job " 
          + params.getJobId());
    // 有且仅有 onStartJob 返回值为 true 时,才会调用 onStopJob 来销毁 job
    // 返回 false 来销毁这个工做
    return false;
  }

  private boolean isNetworkConnected() {
    ConnectivityManager connectivityManager =
      (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
    return (networkInfo != null && networkInfo.isConnected());
  }
  
  private class SimpleDownloadTask extends AsyncTask<JobParameters,
                                                                                                          Void, String> {
    protected JobParameters mJobParam;

    @Override
    protected String doInBackground(JobParameters... params) {
      mJobParam = params[0];
      try {
        InputStream is = null;
        int len = 50;

        URL url = new URL("https://www.google.com");
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setReadTimeout(10000); // 10 sec
        conn.setConnectTimeout(15000); // 15 sec
        conn.setRequestMethod("GET");
        //Starts the query
        conn.connect();
        int response = conn.getResponseCode();
        Log.d(LOG_TAG, "The response is: " + response);
        is = conn.getInputStream();

        // Convert the input stream to a string
        Reader reader = null;
        reader = new InputStreamReader(is, "UTF-8");
        char[] buffer = new char[len];
        reader.read(buffer);
        return new String(buffer);

      } catch (IOException e) {
        return "Unable to retrieve web page.";
      }
    }

    @Override
    protected void onPostExecute(String result) {
      // 当任务完成时,须要调用 jobFinished() 让系统知道完成了哪项任务
      jobFinished(mJobParam, false);
      Log.i("SimpleDownloadTask", result);
    }
  }
}

定义了 JobService 的子类后,而后须要在 AndroidManifest.xml 中进行声明:

<service android:name="pkgName.JobSchedulerService"
    android:permission="android.permission.BIND_JOB_SERVICE" />

最后模拟经过点击 Button 触发 N 个任务,交给 JobService 来处理:

public class FreeTheWakelockActivity extends ActionBarActivity {
  public static final String LOG_TAG = "FreeTheWakelockActivity";

  TextView mWakeLockMsg;
  ComponentName mServiceComponent;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_wakelock);

    mWakeLockMsg = (TextView) findViewById(R.id.wakelock_txt);
    mServiceComponent = new ComponentName(this, MyJobService.class);
    Intent startServiceIntent = new Intent(this, MyJobService.class);
    startService(startServiceIntent);

    Button theButtonThatWakelocks =
      (Button) findViewById(R.id.wakelock_poll);
    theButtonThatWakelocks.setText(R.string.poll_server_button);

    theButtonThatWakelocks.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        pollServer();
      }
    });
  }

  public void pollServer() {
    JobScheduler scheduler =
      (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
    for (int i = 0; i < 10; i++) {
      JobInfo jobInfo = new JobInfo.Builder(i, mServiceComponent)
        .setMinimumLatency(5000) // 5 seconds
        .setOverrideDeadline(60000) // 60 seconds
        .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) // WiFi or data connections
        .build();

      mWakeLockMsg.append("Scheduling job " + i + "!\n");
      scheduler.schedule(jobInfo);
    }
  }
}

官方 demo 地址:https://github.com/googlesamp...

Energy Profiler

Energy Profiler 是 Android Profiler 中的一个组件,可帮助开发者找到应用程序能量消耗的位置。

Energy Profiler 经过监控 CPU、网络和 GPS 传感器的使用状况,并以图形化显示每一个组件使用多少能量。Energy Profiler 还会显示可能影响能耗的系统事件(WakeLock、Alarms、Jobs 和 Location),Energy Profiler 不直接测量能耗,相反,它使用一种模型来估算设备上每种资源的能耗。

能够在 View > Tool Windows > Android Profiler 中打开 Energy Profiler 界面。

Energy Profiler

Energy Profiler 的具体使用可查看 Android 开发文档 - 使用 Energy Profiler 检查能源使用状况。

Energy Profiler 支持 Android 8.0 (API 26) 及以上的系统,Android 8.0 (API 26) 如下请使用 Battery Historian。

Battery Historian

Battery Historian 是一款由 Google 提供的 Android 系统电量分析工具,可以以网页形式展现手机的电量消耗过程。

GitHub 地址:https://github.com/google/bat...

本文以 macOS 环境为例,介绍 Battery Historian 的使用。

Windows 环境请参考:Battery Historian 2.0 for windows 环境搭建。

安装 Docker

手动下载 Docker 安装包,下载连接:https://download.docker.com/m...

安装好以后点击图标运行,在顶部菜单栏能够看到一个鲸鱼图标,说明 Docker 正在运行。

而后在控制台输入:

$ docker --version

看到以下内容,说明 Docker 能够正常使用:

Docker version 19.03.1, build 74b1e89

安装 Battery Historian

经过下面命令安装 Battery Historian:

$ docker run -d -p 9999:9999 bhaavan/battery-historian

上面的步骤都完成以后就能够启动 Battery Historian 了,默认端口是 9999。

以后在浏览器中输入 http://localhost:9999 就能够看到效果,而后上传 bugreport 文件进行分析了。

Battery Historian

获取 bugreport

根据系统版本不一样 bugreport 的获取方式略有差异:

若是 是Android 7.0 及以上版本,经过下面命令来获取 bugreport:

$ adb bugreport bugreport.zip

若是是 Android 6.0 及如下版本,经过下面命令来获取 bugreport:

$ adb bugreport > bugreport.txt

获取到 bugreport 文件以后,咱们就能够将其上传到 Battery Historian 上进行分析,下面是它的输出结果。

Battery Historian

分析结果

在页面的下方咱们能够查看这段时间内系统的状态 system stats,也能够选择某个应用查看应用的状态 app stats。

systrm stats

其中咱们能够看到 Device estimated power use 中显示了估算的应用耗电量值为 0.18%

Battery Historian 还有个比较功能,在首页选择 Switch to Bugreport Comparisor,而后就能够上传两个不一样的 bugreport 文件,submit 以后就能够看到它们的对比结果了,这个功能用来分析同一个应用的两个不一样版本先后的耗电量很是有用。

bugrepor

须要注意的是,通常开始统计数据以前须要使用下面的命令将之前的累积数据清空:

$ adb shell dumpsys batterystats --enable full-wake-history

$ adb shell dumpsys batterystats --reset

上面的操做至关于初始化操做,若是不这么作会有一大堆的干扰的数据,看起来会比较痛苦。

关于 bugreport 相关的知识推荐阅读 Android adb bugreport 工具分析和使用 这篇文章,做者简单地从源码角度分析了 adb bugreport 命令的运行原理,结论是 bugreport 实际上是启动了 dumpstate 服务来输出数据,其中数据来源包括:

  • 系统属性
  • /proc 和 /sys 节点文件
  • 执行 shell 命令得到相关输出
  • logcat 输出
  • Android Framework Services 信息基本使用 dumpsys 命令经过 binder 调用服务中的 dump 函数得到信息

结果分析参考:https://testerhome.com/topics...

阿里P6P7【安卓】进阶资料分享+加薪跳槽必备面试题

相关文章
相关标签/搜索