性能优化十六之Wake_Lock唤醒锁以及JobScheduler使用



前言

      上一篇博客记录了电量优化中的第一种优化,把一些不需要及时和用户交互的一些操作,放到当用户插上电源的时候。根据自己目前的知识了解,只知道三种优化方式,第二种和第三种方式接下来进行介绍。

第二种方式:网络类型选择优化

      目前大部分手机都支持4G网络,殊不知蜂窝移动信号是在所有的网络类型中是最消耗电量的,很多人在使用手机的过程中,发现如果一直在使用4G移动网络,电量会持续不了多久就没电了,而相对来说WIFI会比蜂窝移动信号的电量消耗会小很多,所以我们在开发过程中可以将某些操作放在连接WIFI后进行操作。如何去判读网络类型的代码例子,网上都有。

第三种方式:wake_lock

      wakelock是个什么东西呢?查了很多资料了解到它是一个唤醒锁,什么是唤醒锁?它主要是相对系统的休眠而言的,意思就是我的程序给CPU加了这个锁那系统就不会休眠了,这样做的目的是为了全力配合我们程序的运行。有的情况如果不这么做就会出现一些问题,比如微信等及时通讯的心跳包会在熄屏不久后停止网络访问等问题。所以微信里面是有大量使用到了wake_lock锁(可以利用WLD进行测试)。

使用场景一:保持屏幕常亮

      当Android设备空闲时,屏幕会变暗,然后关闭屏幕,最后会停止CPU的运行,这样可以防止电池电量掉的快。在休眠过程中自定义的Timer、Handler、Thread、Service等都会暂停。但有些时候我们需要改变Android系统默认的这种状态:比如玩游戏时我们需要保持屏幕常亮,比如一些下载操作不需要屏幕常亮但需要CPU一直运行直到任务完成。

1、保持屏幕常亮,最好的方式是在Activity中使用FLAG_KEEP_SCREEN_ON 的Flag。
public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
    }
}

优点:这个方法的好处是不像唤醒锁(wake locks),需要一些特定的权限(permission)。并且能正确管理不同app之间的切换,不用担心无用资源的释放问题(唤醒锁如何使用下面介绍)。

注意:一般不需要人为的去掉flag,WindowManager会管理好程序进入后台回到前台的操作。如果确实需要手动清掉常亮的flag,使用
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
2、布局文件中设置屏幕常亮:

另一个方式是在布局文件中使用android:keepScreenOn属性:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:keepScreenOn="true">
< /RelativeLayout>

android:keepScreenOn = ” true “的作用和FLAG_KEEP_SCREEN_ON一样。但是使用代码的好处是你允许你在需要的地方关闭屏幕。

使用场景二:保持CPU运行

      需要使用PowerManager这个系统服务的唤醒锁(wake locks)特征来保持CPU处于唤醒状态。唤醒锁允许程序控制宿主设备的电量状态。创建和持有唤醒锁对电池的续航有较大的影响,所以,除非是真的需要唤醒锁完成尽可能短的时间在后台完成的任务时才使用它。比如在Acitivity中就没必要用了。如果需要关闭屏幕,使用上述FLAG_KEEP_SCREEN_ON。
      只有一种合理的使用场景,就是在使用后台服务需要在屏幕关闭情况下hold住CPU完成一些工作。这时就需要使用唤醒锁,如果不使用唤醒锁来执行后台服务,当CPU在未来的某个时刻休眠导致某个时刻任务会停止,这是我们不想看到的。 (有的人可能认为我以前写的后台服务运行得挺好的,1.可能是你的任务时间比较短;2.可能CPU被手机里面很多其他的软件一直在唤醒状态。)。下面是很多网友有同样的问题:
这里写图片描述

唤醒锁可划分为并识别四种用户唤醒锁:

标记值                   CPU  屏幕  键盘
PARTIAL_WAKE_LOCK       开启  关闭  关闭
SCREEN_DIM_WAKE_LOCK    开启  变暗  关闭
SCREEN_BRIGHT_WAKE_LOCK 开启  变亮  关闭
FULL_WAKE_LOCK          开启  变亮  变亮
请注意,自 API 等级 17 开始,FULL_WAKE_LOCK 将被弃用,应用应使用FLAG_KEEP_SCREEN_ON
使用方法一:

第一步就是添加唤醒锁权限:

< uses-permission android:name="android.permission.WAKE_LOCK" />

第二步直接使用唤醒锁:

PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,"MyWakelockTag");
wakeLock.acquire();

注意:在使用该类的时候,必须保证acquire和release是成对出现的,需要在任务执行完成后调用release方法。
第三步释放唤醒锁:

private void releaseWakeLock() {
        if (mWakelock.isHeld()) {
            mWakelock.release();//记得释放CPU锁
            wakelock_text.append("释放锁!");
       }
}
使用方法二:

      虽然上面的方式很简单,但是推荐的方式是使用WakefulBroadcastReceiver:使用广播和Service(典型的IntentService)结合的方式可以让你很好地管理后台服务的生命周期。

      WakefulBroadcastReceiver是BroadcastReceiver的一种特例。它会为你的APP创建和管理一个PARTIAL_WAKE_LOCK 类型的WakeLock。WakefulBroadcastReceiver把工作交接给service(通常是IntentService),并保证交接过程中设备不会进入休眠状态。如果不持有WakeLock,设备很容易在任务未执行完前休眠。最终结果是你的应用不知道会在什么时候能把工作完成,相信这不是你想要的。

第一步注册:

< receiver android:name=".MyWakefulReceiver">receiver>

第二步使用startWakefulService()方法来启动服务,与startService()相比,在启动服务的同时,并启用了唤醒锁。

public class MyWakefulReceiver extends WakefulBroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {        
        // Start the service, keeping the device awake while the service is        
        // launching. This is the Intent to deliver to the service.        
         Intent service = new Intent(context, MyIntentService.class);        
         startWakefulService(context, service);  
    }
}

第三步当后台服务的任务完成,要调用MyWakefulReceiver.completeWakefulIntent()来释放唤醒锁。

public class MyIntentService extends IntentService {
    public static final int NOTIFICATION_ID = 1;
    private NotificationManager mNotificationManager;
    NotificationCompat.Builder builder;

    public MyIntentService() {
        super("MyIntentService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        Bundle extras = intent.getExtras();        
        // Do the work that requires your app to keep the CPU running.        
        // 唤醒CPU处理工作
        // Release the wake lock provided by the WakefulBroadcastReceiver.        
         MyWakefulReceiver.completeWakefulIntent(intent);    
    }
}

下面是从网上看到的一些不处理CPU唤醒遇到的一些问题:

1.向服务器轮询的代码不执行。
      曾经做一个应用,利用Timer和TimerTask,来设置对服务器进行定时的轮询,但是发现机器在某段时间后,轮询就不再进行了。查了很久才发 现是休眠造成的。后来解决的办法是,利用系统的AlarmService来执行轮询。因为虽然系统让机器休眠,节省电量,但并不是完全的关机,系统有一部 分优先级很高的程序还是在执行的,比如闹钟,利用AlarmService可以定时启动自己的程序,让cpu启动,执行完毕再休眠。
2.后台长连接断开。
      最近遇到的问题。利用Socket长连接实现QQ类似的聊天功能,发现在屏幕熄灭一段时间后,Socket就被断开。屏幕开启的时候需进行重连,但 每次看Log的时候又发现网络是链接的,后来才发现是cpu休眠导致链接被断开,当你插上数据线看log的时候,网络cpu恢复,一看网络确实是链接的, 坑。最后使用了PARTIAL_WAKE_LOCK,保持CPU不休眠。
3.调试时是不会休眠的。
      让我非常郁闷的是,在调试APP的时候,就发现,有时Socket会断开,有时不会断开,后来才搞明白,因为我有时是插着数据线进行调试,有时拔掉数据线,这 时Android的休眠状态是不一样的。而且不同的机器也有不同的表现,比如有的机器,插着数据线就会充电,有的不会,有的机器的设置的充电时屏幕不变暗 等等,把自己都搞晕了。其实搞明白这个休眠机制,一切都好说了。

定时唤醒解决方法:利用Android自带的定时器AlarmManager实现:

Intent intent = new Intent(mContext, ServiceTest.class);
PendingIntent pi = PendingIntent.getService(mContext, 1, intent, 0);
AlarmManager alarm = (AlarmManager) getSystemService(Service.ALARM_SERVICE);
if(alarm != null)
{
    alarm.cancel(pi);
    // 闹钟在系统睡眠状态下会唤醒系统并执行提示功能
    alarm.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 1000, 2000, pi);// 确切的时间闹钟//alarm.setExact(…);
    //alarm.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), pi);
}

      该定时器可以启动Service服务、发送广播、跳转Activity,并且会在系统睡眠状态下唤醒系统。所以该方法不用获取电源锁和释放电源锁。
      注意:在19以上版本,setRepeating中设置的频繁只是建议值(6.0 的源码中最小值是60s),如果要精确一些的用setWindow或者setExact。

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进入休眠状态,就会成为待机电池杀手。比如微信内部就大量使用了wakelock唤醒锁,理所当然的成了耗电排行第一。

      那么Wake Lock API有啥用呢?比如心跳包从请求到应答,比如断线重连重新登陆这些关键逻辑的执行过程,就需要Wake Lock来保护。而一旦一个关键逻辑执行成功,就应该立即释放掉Wake Lock了。两次心跳请求间隔5到10分钟,基本不会怎么耗电。除非网络不稳定,频繁断线重连,那种情况不多。

      AlarmManager 是Android 系统封装的用于管理 RTC 的模块,RTC (Real Time Clock) 是一个独立的硬件时钟,可以在 CPU 休眠时正常运行,在预设的时间到达时,通过中断唤醒 CPU。(极光推送就是利用这个来做的。)

总结
1. 关键逻辑的执行过程,就需要Wake Lock来保护。如断线重连重新登陆
2. 休眠的情况下如何唤醒来执行任务?用AlarmManager。如推送消息发送心跳包,获取信息
注意:如果请求网络很差,会要很长的时间,一般我们谷歌建议一定要设置请求超时时间。

最重要的注意事项:
      Android开发中,可能会出现AlarmManager在手机休眠时无法唤醒Service的问题。
      问题的提出:
      一个app,需要后台保持发送心跳包。由于锁屏后CPU休眠,导致心跳包线程被挂起,所以尝试使用alarmManager定时唤醒Service发送心跳包,当传入的时间参数很短的时候,例如2500ms,很长时间才会去唤醒一次,而且间隔时间是不固定的。
      原因:
      首先2.5s一次唤醒对于手机电池的消耗是多么的恐怖,做后台应用的时候需要考虑下是否会影响手机的正常休眠(深睡眠),各个手机厂家为了对付频繁唤醒的app,都开发了心跳对齐,对于超过指定的频率就会被厂商给屏蔽或者被心跳对齐了。

JobScheduler引入:

      当遇到大量高频次的CPU唤醒及操作,我们该如何去优化,频繁的唤醒肯定导致电量消耗很大。
      这里我们可以通过谷歌的JobScheduler来实现,将一些频繁唤醒的任务集中到一起,这样只需要唤醒一次cpu,就可以完成所有操作。
      使用方法:
      MyJobService.java:

public class MyJobService extends JobService {
    private static final String LOG_TAG = "MyJobService";

    @Override
    public void onCreate() {
        super.onCreate();
        Log.i(LOG_TAG, "MyJobService created");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.i(LOG_TAG, "MyJobService destroyed");
    }

    @Override
    public boolean onStartJob(JobParameters params) {
        // This is where you would implement all of the logic for your job. Note that this runs
        // on the main thread, so you will want to use a separate thread for asynchronous work
        // (as we demonstrate below to establish a network connection).
        // If you use a separate thread, return true to indicate that you need a "reschedule" to
        // return to the job at some point in the future to finish processing the work. Otherwise,
        // return false when finished.
        Log.i(LOG_TAG, "Totally and completely working on job " + params.getJobId());
        // First, check the network, and then attempt to connect.
        if (isNetworkConnected()) {
            new SimpleDownloadTask() .execute(params);
            return true;
        } else {
            Log.i(LOG_TAG, "No connection on job " + params.getJobId() + "; sad face");
        }
        return false;
    }

    @Override
    public boolean onStopJob(JobParameters params) {
        // Called if the job must be stopped before jobFinished() has been called. This may
        // happen if the requirements are no longer being met, such as the user no longer
        // connecting to WiFi, or the device no longer being idle. Use this callback to resolve
        // anything that may cause your application to misbehave from the job being halted.
        // Return true if the job should be rescheduled based on the retry criteria specified
        // when the job was created or return false to drop the job. Regardless of the value
        // returned, your job must stop executing.
        Log.i(LOG_TAG, "Whelp, something changed, so I'm calling it on job " + params.getJobId());
        return false;
    }

    /** * Determines if the device is currently online. */
    private boolean isNetworkConnected() {
        ConnectivityManager connectivityManager =
                (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
        return (networkInfo != null && networkInfo.isConnected());
    }

    /** * Uses AsyncTask to create a task away from the main UI thread. This task creates a * HTTPUrlConnection, and then downloads the contents of the webpage as an InputStream. * The InputStream is then converted to a String, which is logged by the * onPostExecute() method. */
    private class SimpleDownloadTask extends AsyncTask<JobParameters, Void, String> {

        protected JobParameters mJobParam;

        @Override
        protected String doInBackground(JobParameters... params) {
            // cache system provided job requirements
            mJobParam = params[0];
            try {
                InputStream is = null;
                // Only display the first 50 characters of the retrieved web page content.
                int len = 50;

                URL url = new URL("https://www.google.com");
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                conn.setReadTimeout(10000); //10sec
                conn.setConnectTimeout(15000); //15sec
                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(mJobParam, false);
            Log.i(LOG_TAG, result);
        }
    }
}

MainActivity.java:

    private ComponentName serviceComponent = new ComponentName(this,MyJobService.class);
        //优化
        JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
        for (int i = 0; i < 500; i++) {
            JobInfo jobInfo = new JobInfo.Builder(i,serviceComponent)
                    .setMinimumLatency(5000)//5秒 最小延时、
                    .setOverrideDeadline(60000)//maximum最多执行时间
             //JobInfo.NETWORK_TYPE_UNMETERED//免费的网络---wifi 蓝牙 USB
                    .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)//任意网络---wifi
                    .build();
            jobScheduler.schedule(jobInfo);
        }

      如果利用Job Scheduler,应用需要做的事情就是判断哪些任务是不紧急的,可以交给Job Scheduler来处理,Job Scheduler集中处理收到的任务,选择合适的时间,合适的网络,再一起进行执行。