13(下) 继续进阶---你还应该掌握高级技巧

动漫美少女.jpg

13.5 创建定时任务

Android中的定时任务一般有两种实现方式,一种是使用Java API里提供的Timer类,一种是使用AndroidAlarm机制。这两种方式在多数情况下都能实现类似的效果,但Timer有一个明显的短板,它并不太适用于那些需要长期在后台运行的定时任务。我们都知道为了能让电池更加耐用,每种手机都会有自己的休眠策略,Android手机就会在长时间不操作的情况下自动让CPU进入到睡眠状态,这就有可能导致Timer中的定时任务无法正常运行。而Alarm则具有唤醒CPU的功能,它可以保证在大多数情况下需要执行定时任务的时候CPU都能正常工作。需要注意: 这里唤醒CPU和唤醒屏幕完全不是一个概念,千万不要产生混淆。

13.5.1 Alarm机制

Alarm机制的用法主要就是借助了AlarmManager类来实现的。这个类和NotificationManager有点类似,都是通过调用ContextgetSystemService()方法来获取实例的,只是这里需要传入的参数是Context.ALARM_SERVICE

获取一个AlarmManager的实例:
AlarmManager manager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);

接下来调用AlarmManagerset()方法就可以设置一个定时任务了,比如说想要设定一个任务在10秒钟后执行

long triggerAtTime = SystemClock.elapsedRealtime() + 10 * 1000;
manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,triggerAtTime,pendingIntent);

set()方法中需要传入的3个参数稍微有点复杂。

第一个参数是一个整形参数,用于指定AlarmManager的工作类型,有四种值可选,

ELAPSED_REALTIME: 让定时任务的触发时间从系统开机开始算起,但不会唤醒CPU

ELAPSED_REALTIME_WAKEUP: 表示让定时任务的触发时间从系统开机开始算起,但会唤醒CPU

RTC: 让定时任务的触发时间从1970110点开始算起,但不会唤醒CPU

RTC_WAKEUP: 让定时任务的触发时间从1970110点开始算起,但会唤醒CPU

使用SystemClock.elapsedRealtime()方法可以获取到系统开机至今所经历时间的毫秒数,使用System.currentTimeMillis()方法可以获取到从1970110点至今所经历时间的毫秒数。

第二个参数就好理解多了,就是定时任务触发的时间,以毫秒为单位。如果第一个参数使用的是ELAPSED_REALTIMEELAPSED_REALTIME_WAKEUP,则这里传入开机至今的时间再加上延迟至今的时间。如果第一个参数使用的是RTCRTC_WAKEUP则这里传入1970110点至今的时间再加上延迟至今的时间。

第三个参数是一个PendingIntent,这里我们一般会调用getService()方法或者getBroadcast()方法来获取一个能够执行服务或广播的PendingIntent。这样当定时任务被触发的时候,服务的onStartCommand()方法或广播接收器的onReceive()方法就可以得到执行。

设定一个任务在10秒钟后执行可以写成

long triggerAtTime = System.currentTimeMillis() + 10 * 1000;
manager.set(AlarmManager.RTC_WAKEUP,triggerAtTime,pendingIntent);

如果我们要实现一个长时间在后台定时运行的服务,首先新建一个普通的服务,起名叫LongRunningService,然后将触发定时任务的代码写到onStartCommand()方法中。

public class LongRunningService extends Service
{

    @Override
    public IBinder onBind(Intent intent)
    {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId)
    {
        new Thread(new Runnable()
        {
            @Override
            public void run()
            {
                //执行具体的逻辑操作
            }
        }).start();

        AlarmManager manager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
        int anHour = 60 * 60 * 1000; //这是一小时的毫秒数
        long triggerAtTime = SystemClock.elapsedRealtime() + anHour;
        Intent i = new Intent(this,LongRunningService.class);
        PendingIntent pi = PendingIntent.getService(this,0,i,0);
        manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,triggerAtTime,pi);
        return super.onStartCommand(intent, flags, startId);
    }
}

我们先是在onStartCommand()方法中开启了一个子线程,这样就可以在这里执行具体的逻辑操作了。之所以要在子线程里执行逻辑操作,是因为逻辑操作也是需要耗时的,如果放在主线程里执行可能会对定时任务的准确性造成轻微的影响。

创建线程之后的代码就是我们刚刚讲解的Alarm机制的用法了。首先获取到了AlarmManager的实例,然后定义任务的触发时间为一小时后,再使用PendingIntent指定处理定时任务的服务为LongRunningService,最后调用set()方法完成设定。

这样我们就将一个长时间在后台定时运行的服务成功实现了。因为一旦启动了LongRunningService,就会在onStartCommand()方法中设定一个定时任务,这样一小时后将会再次启动LongRunningService,从而也就形成了一个永久的循环,保证LongRunningService的onStartCommand()方法可以每隔一小时就执行一次。

最后,只需要在你想要启动定式服务的时候调用如下代码即可:

Intent intent = new Intent(context,LongRunningService.class);
context.startService(intent);

另外需要注意的是,从Android4.4系统开始,Alarm任务的触发时间将会变得不正确,有可能会延迟一段时间后任务才能得到执行。这并不是个bug,而是系统在耗电性方面进行的优化。系统会自动检测目前有多少Alarm任务存在,然后将触发时间相近的几个任务放在一起执行,这就可以大幅度地减少CPU被唤醒的次数,从而有效延长电池的有效时间。

如果你要求Alarm任务的执行时间必须准确无误,Android仍然提供了解决方案。使用AlarmManager的setExact()方法来替代set()方法,就基本可以保证任务能够准时执行了

13.5.2 Doze模式

虽然Android的每个系统版本都在手机电量方面努力进行优化,不过一直没能解决后台服务泛滥,手机电量消耗过快的问题。于是在Android6.0系统中,谷歌加入了一个全新的Doze模式,从而可以极大幅度地延长电池的使用寿命。

首先看一下到底什么是Doze模式。当用户的设备是Android6.0或以上系统时,如果该设备未插接电源,处于静止状态(Android7.0中删除了这一条),且屏幕关闭了一段时间之后,就会进入到Doze模式。在Doze模式下,系统会对CPU,网络,Alarm等活动进行限制,从而延长电池的使用寿命。

当然,系统并不会一直处于Doze模式,而是会间歇性地退出Doze模式一小段时间,在这段时间中,应用就可以去完成它们的同步操作,Alarm任务,等等。

Doze模式的工作过程.jpg

左边是未插电源,设备静止,屏幕关闭,接着是短暂退出Doze模式

可以看到,随着设备进入Doze模式的时间越长,间歇性地退出Doze模式的时间间隔也会越长。因为如果设备长时间不使用的话是没必要频繁退出Doze模式来执行同步等操作的,Android在这些细节上的把控使的电池寿命进一步得到了延长。

接下来我们具体看一看Doze模式下有哪些功能会受到限制吧。

  • 网络访问被禁止
  • 系统忽略唤醒CPU或者屏幕操作
  • 系统不再执行WIFI扫描
  • 系统不再执行同步服务
  • Alarm任务将会在下次退出Doze模式的时候执行

注意其中的最后一条,也就是说,在Doze模式下,我们的Alarm任务将会变得不准时。当然,这在大多数情况下都是合理的,因为只有当用户长时间不使用手机的时候才会进入Doze模式,通常在这种情况下对Alarm任务的准时性要求并没有那么高。

不过,如果你真的有非常特殊的需求,要求Alarm任务即使在Doze模式下也必须正常执行,Android还是提供了解决方案。调用AlarmManagersetAndAllowWhileIdle()setExactAndAllowWhileIdle()方法就能让定时任务即使在Doze模式下也能正常执行了,这两个方法之间的区别和set(),setExact()方法之间的区别是一样的。

13.6

Android7.0系统中引入了一个非常有特色的功能---多窗口模式

13.6.1 进入多窗口模式

我们不用编写任何额外的代码来让应用程序支持多窗口模式。

如何才能进入到多窗口模式?

手机底部有一个正方形的Overview按钮,它的作用是打开一个最近访问过的活动或任务的列表界面。

我们可以通过以下两种方式进入多窗口模式。

  • 在Overview列表界面长按任意一个活动的标题,将该活动拖动到屏幕突出显示的区域,则可以进入多窗口模式。

  • 打开任意一个程序,长按Overview按钮,也可以进入多窗口模式。

我们还可以将模拟器旋转至水平方向,这样上下分屏的多窗口模式会自动转换成左右分屏的多窗口模式。

如果想要退出多窗口模式,只需要再次长按Overview按钮,或者将屏幕中央的分割线向屏幕任意一个方向拖动到底即可。

可以看出,再多窗口模式下,整个应用的界面会缩小很多,那么编写程序时就应该多考虑使用match_parent属性,RecyclerView,ListView,ScrollView等控件,来让应用的界面能够更好地适配各种不同尺寸的屏幕,尽量不要出现屏幕尺寸变化过大时界面就无法正常显示的情况。

13.6.2 多窗口模式下的生命周期

其实多窗口模式并不会改变活动原有的生命周期,只是会将用户最近交互过的那个活动设置为运行状态,而将多窗口模式下另外一个可见的活动设置为暂停状态。如果这时用户又去和暂停的活动进行交互,那么该活动就变成运行状态,之前处于运行状态的活动变成暂停状态。

我们选择MaterialTest项目和LBSTest项目。

先启动MaterialTest项目:

MaterialTest: onCreate
MaterialTest: onStart
MaterialTest: onResume

然后长按Overview按钮,进入多窗口模式:

MaterialTest: onPause
MaterialTest: onStop
MaterialTest: onDestory
MaterialTest: onCreate
MaterialTest: onStart
MaterialTest: onResume
MaterialTest: onPause

可以看到MaterialTest经历了一个重建的过程。其实这是个正常现象,因为进入到多窗口模式后活动的大小发生了比较大的变化,此时默认是会重新创建活动的。进入多窗口模式后,MaterialTest变成了暂停状态。

接着在Overview列表界面选中LBSTest程序

LBSTest: onCreate
LBSTest: onStart
LBSTest: onResume

现在LBSTset变成了运行状态。

我们随意操作一下MaterialTest程序:

LBSTest: onPause
MaterialTest: onResume

说明LBSTest变成了暂停状态,MaterialTest变成了运行状态。

这和我们在本小节开头所分析的生命周期行为是一致的。

在多窗口模式下,用户仍然可以看到处于暂停状态的应用,那么像视频播放器之类的应用在此时就应该能继续播放视频才对。因此,我们最好不要再活动的onPause()方法中去处理视频播放的暂停逻辑,而是应该在onStop()方法中去处理,并且在onStart()方法恢复视频的播放。

另外,针对于进入多窗口模式时活动会被重新创建,如果你想改变这一默认行为,可以在AndroidManifest.xml活动中进行如下配置。

<activity
   android:name= ".MainActivity"
   android:label= "Fruits"
   android:configChanges="orientation|keyboardHidden|screenSize|screenLayout">
</activity>

加入了这行配置之后,不管是进入多窗口模式,还是横竖屏切换,活动都不会被重新创建,而是会将屏幕发生变化的事件通知到Activity的onConfigurationChanged()方法当中。因此,如果你想在屏幕发生变化的时候进行相应的逻辑处理,那么在活动中重写onConfigurationChanged()方法即可。

13.6.3 禁用多窗口模式

多窗口模式虽然功能非常强大,但是未必就适用于所有的程序。

因此,Android还是给我们提供了禁用多窗口模式的选项,如果你非常不希望自己的应用能够在多窗口模式下运行,那么就可以将这个功能关闭掉。

只需要在AndroidManifest.xml的<application>或<activity>标签中加入如下属性即可:

android:resizeableActivity=["true" | "false"];

其中,true表示应用支持多窗口模式,false表示应用不支持多窗口模式,如果不配置这个属性,那么默认值为true。

配置好之后,打开程序。

现在是无法进入到多窗口模式的,而是屏幕下方还会弹出一个Toast提示来告知用户,当前应用不支持多窗口模式。

虽说android:resizeableActivity这个属性的用法很简单,但是它还存在着一个问题,就是这个属性只有当项目的targetSdkVersion指定成24或者更高的时候才会有用,否则这个属性是无效的。那么比如说我们将项目的targetSdkVersion指定成23,这个是够尝试进入多窗口模式。

可以看到,虽说界面上弹出了一个提示,告知我们此应用在多窗口模式下可能无法正常工作,但还是进入了多窗口模式。

针对这个情况,还有一种解决方案,Android规定,如果项目指定的targetSdkVersion低于24,并且活动是不允许横竖屏切换的,那么该应用也将不支持多窗口模式。

默认情况下,我们的应用都是可以随着手机的旋转自由地横竖屏切换,如果想要应用不允许横竖屏切换,那么就需要在AndroidManifest.xml的<activity>标签中加入如下属性即可:

android:screenOrientation=["portrail" | "landscape"];

portrail表示活动只允许竖屏,landscape表示活动只允许横屏,当然android:screenOrientation还有很多的可选值,"portrail" | "landscape"是最常用的。

13.7 Lambda表达式

Java8中着实引入了一些非常有特色的功能,如Lambda表达式,stream API,接口默认实现

stream API,接口默认实现等特性都是只支持Android7.0及以上的系统。而Lambda表达式却最低兼容到Android2.3系统,几乎覆盖了所有Android手机。

Lambda表达式本质上是一种匿名方法,它既没有方法名,也没有访问修饰符和返回值类型,使用它来编写代码将会更加简洁,也更加易读。

如果想要在Android项目中使用Lambda表达式或者Java8的其他新特性,首先我们需要在app/build.gradle中添加如下配置。

android {
    ·············
        jackOptions.enabled = true
    }
compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}

之后就可以使用Lambda表达式了。比如说传统情况下开启一个子线程的写法如下:

new Thread(new Runnable(){
    @Override
    public void run(){
      // 处理具体的逻辑
    }
}).start();

而使用Lambda表达式则可以这样使用:

new Thread(() ->{
    //处理具体的逻辑
}).start();

因为Thread类的构造函数接收的参数是一个Runnable接口,并且该接口中只有一个待实现方法。我们看一下Runnable接口的源码

public interface Runnable {
    
    public abstract void run();

}

凡是这种只有一个待实现方法的接口,都可以使用Lambda表达式的写法。比如说,通常创建一个类似于上述接口的匿名类实现需要这样写:

Runnable runnable = new Runnable(){
    @Override
    public void run(){
        //添加具体的逻辑
    }
};

而有了Lambda表达式之后,我们可以这样写:

Runnable runnable1 = () -> {
   //添加具体的实现  
};

接下来我们尝试自定义一个接口,然后再使用Lambda表达式的方式进行实现:

public interface MyListener {
    String doSomething(String a,int b);
}

MyListener接口中也只有一个待实现方法,这和Runnable接口的结构是基本一致的。唯一不同的是,MyListener中的`doSomething()方法是有参数并且有返回值的。

 MyListener listener = (String a,int b) -> {
      String result = a + b;
      return result;
};

doSomething()方法的参数直接写在括号里就可以了,而返回值仍然像往常一样,写在具体实现的最后一行即可。

另外,Java还可以根据上下文自动推断出Lambda表达式中的参数类型,上面的也可以简化为:

 MyListener listener1 = (a,b) ->{
      String result = a + b;
      return result;
};

Java将会自动推断出参数aString类型,参数bint类型,从而使得我们的代码变得更加精简了。

看个例子

public void hello(MyListener listener) {
        String a = "Hello Lambda";
        int b = 1024;
        String result = listener.doSomething(a,b);
        Log.d(TAG, result);
}

在调用hello()这个方法可以这样写;

hello((a,b) -> {
     String result = a + b;
     return result;
});

那么doSomething()方法就会将ab两个参数进行相加,结果是Hello Lambda1024

Android中的应用:

button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
   @Override
    public void onClick(View v){
          // 处理点击事件
    }
});

使用Lambda后表达式后为:

button = (Button) findViewById(R.id.button);
button.setOnClickListener((v) -> {
     // 处理点击事件
});

另外,当接口的待实现方法有且只有一个参数的时候,还可以进一步简化

button = (Button) findViewById(R.id.button);
button.setOnClickListener(v -> {
     // 处理点击事件
});        
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容