0%

第13章: 继续进阶——你还应该掌握的高级技巧

全局获取Context技巧

主要就是自定义 Application ,在 Application 中实现全局获取Context,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class MyApplication extends Application{
private static Context mContext;

@Override
public void onCreate(){
mContext = getApplicationContext();
}

public static Context getContext(){
return mContext;
}
}

使用Intent传递对象

Intent的putExtra()方法传递数据的时候,支持的数据类型是有限的,虽然常用的一些数据类型它都支持,但是当你想传递一些自定义对象的时候,就会无从下手。其实使用Intent来传递对象通常有两种实现方式:Serializable和Parcelable。

Serializable 方式

Serializable是序列化的意思,表示将一个对象转换成可存储或可传输的状态。至于序列化的方法也很简单,只需要实现Serializable这个接口就行,如以下的Person类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Person implements Serializable{
//TODO 这行在书中是没有的,实际上我们得加上,用于区分版本这个类的版本,不同的这个id不能反序列回来
//如果不写,系统会自动生成,但是如果改动了里面的属性(增加或者减少或者更改了属性),系统生成的这个值会改变
//(注意,如果没有实质改动(如只是改变属性的位置,或者在类中添加了空格)则值不会改变
private static final long serialVersionUID =1L;

private String name;
private int age;

public String getName(){
return name;
}

public void setName(String name){
this.name = name;
}

public int getAge(){
return age;
}

public void setAge(int age){
this.age = age;
}
}

如果要在Activity之间传递的话,只需要简单的几行代码即可:

1
2
3
4
5
6
Person person = new Person();
person.setName("Tom");
person.setAge("20");
Intent intent = new Intent(this,SecondActivity.class);
intent.putExtra("person_data",person);
startActivity(intent);

可以看到我们穿件了Person实例,之后直接将它传入putExtra()方法中了,只是因为Person实现了Serializable接口,所以才可以这么写。接下来要在SecondActivity中获取这个对象也很简单:

1
Person person = (Person)getIntent().getSerializableExtra("person_data");

这里调用了getSerializableExtra方法来获取通过参数传递过来的序列化对象,接着再向下转型得到Person对象,就成功实现了Intent传递对象。

Parcelable方式

除了Serializable之外,使用Parcelable也可以实现相同的效果,不过不同于将对象进行序列化,Parcelable方式的实现原理是将一个完整的对象进行分解,而分解后的每一部分都是Intent所支持的数据类型,这样也就实现了传递对象的功能了。下面修改下Person类的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Person implements Parcelable{

private String name;
private int age;

。。。

@Override
public int describeContents(){
return 0;
}

@Override
public void writeToParcel(Parcel dest,int flags){
dest.writeString(name);//写出name
dest.writeInt(age);//写出age
}

public static final Parcelable.Creator<Person> CREATOR = new Parcelable.Creator<Person>(){

@Override
public Person createFromParcel(Parcel source){
Person person = new Person();
person.name = source.readString();//读取name
person.age = source.readInt();
return person;
}

@Override
public Person[] newArray(int size){
return new Person[size];
}
};
}

Parcelable 的实现方式要稍微复杂些,首先继承 Parcelable 接口,这样就必须重写 describeContents() 和 writeToParcel() 两个方法。 describeContents 中直接返回0即可,writeToParcel中需要将Person类中的字段一一写出。除此之外,还得再Person类中提供一个 CREATOR 常量。接下来,我们仍然可以通过前面相同的代码来传递Person对象,只不过在SecondActivity中获取对象的时候需要稍加改动:

1
Person person = (Person)getIntent().getParcelableExtra("person_data");

一般来说,Serializable方式比较简单,但是这会把整个对象序列化,因此效率比Parcelable低一些,所以更加推荐Parcelable方式。

定制自己的日志工具

实用性不太强,公司项目有更强大的日志工具,因此 略

调试Android程序

已经掌握,略

创建定时任务

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

Alarm机制

Alarm机制用法并不复杂,主要借助于AlarmManager实现,跟NotificationManager有点类似,获取实例的方法如下所示:

1
AlarmManager manager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);

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

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

第一个参数是整型参数,用于指定AlarmManager的工作类型,有4种值可选:ELAPSED_REALTIME(让定时任务的触发时间从系统开机开始算,但不会唤醒CPU)、ELAPSED_REALTIME_WAKEUP(表示定时任务从系统开机开始算起,但会唤醒CPU)、RTC(让定时任务触发时间从1970年1月1日0点开始算起,但不会唤醒CPU)、RTC_WAKEUP(让定时任务触发时间从1970年1月1日0点开始算起,但会唤醒CPU)。可以使用SystemClock.elapsedRealtime()获取到系统开机至今所经历的时间的毫秒数。Systemt.currentTimeMillis()可以获取到1970年1月1日0点至今所经历的毫秒数。第二个参数就是定时任务触发的时间,如果第一个参数是ELAPSED_REALTIME或者ELAPSED_REALTIME_WAKEUP,则传入开机至今时间加上延迟执行的时间;如果是RTC或者RTC_WAKEUP,则传入1970年1月1日0点至今的时间再加上延迟执行的时间。第三个参数不多说。

那么如果要实现一个长时间在后台定时运行的服务要如何做呢,其实只要建立一个普通服务,然后将触发定时任务的代码写到onStartCommand()方法中,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class LongRuningService extends Service{

...

@Override
public int onstartCommand(Intent intent,int flags,int startId){

//TODO 之所以要在子线程里面执行逻辑操作,是因为逻辑操作需要耗时,放在主线程中可能会对定时任务准确性产生轻微影响。
new Thread(new Runnable(){
@Override
public void run(){
//执行具体的逻辑操作
}
}).start();

AlarmManager manager = (AlarmManager)getSystemService(ALARM_SERVICE);
int anHour = 60 * 60 * 1000;//一小时
long triggerAtTime = SystemClock.elapsedRealtime() + anHour;
Intent i = new Intent(this,LongRunningService(this,0,i,0));
//TODO 注意,从4.4开始,Alarm任务的触发将会变得不准确,有可能会延迟一段时间后任务才能得到执行,这不是bug,
//而是系统在耗电方面的优化,系统会自动检测目前有多少Alarm任务,然后将触发时间相近的几个任务放在一起执行,可以
//大幅度减少CPU被唤醒的次数,从而延长电池使用。如果要求Alarm任务执行时间必须准确无误,则使用 setExact()方法替代
//下面的set()方法。
manager.set(AlarmManager.ELAPSED_REALTIME_WEKEUP,triggerAtTime,pi);
return super.onStartCommand(intent,flags,startId);
}
}

这样,一旦启动了LongRuningService,就会设定一个定时任务,一个小时后会再次启动LongRuningService,形成一个永久循环。

Doze模式

在Android 6.0中,google加入了Doze模式,及大幅度延长电池使用寿命。主要表现为:如果设备未插电源,处于静止(7.0后删除这条件),并且屏幕关闭了一段时间之后,就会进入Doze模式,系统会对CPU、网络、Alarm等活动限制,当然,系统还会间歇性地退出Doze一小段时间,在这段时间,应用可以去完成他们的同步操作、Alarm任务等。如下图所示:

Doze模式示意图

可以看到,随着设备进入Doze模式的时间越长,间歇性退出Doze模式的时间间隔也会越来越长,因为如果设备长时间不使用的话,没必要频繁退出Doze。以下列出Doze模式下有具体哪些功能会收到限制:

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

不过,如果你真有非常特殊需求,要求Alarm任务在Doze模式也必须正常执行,可以调用AlarmManager的setAndAllowWhileIdle()或setExactAcnAllowWhileIdle()方法就能让定时任务即使在Doze模式下也能正常执行了,这两个方法之间的区别和set()、setExact()方法之间的区别一样。

*** 多窗口模式

在一个屏幕上,同时显示两个app界面。切换到多窗口模式,Activity会经历重新创建的过程。其实这是正常现象,进入多窗口模式后,Activity的大小发生了比较大的变化,此时默认会重新创建活动的。除此之外,像横竖屏也会重新创建活动。如果此时去操作另一个窗口,则当前窗口会执行onPause,而另一个窗口会执行onResume,这很好理解,因为两个窗口都是可见的,所以只会执行到onPause即可。因此,在考虑多窗口模式下,用户仍然可以看到处于暂停状态的应用,那么像视频播放器之类的应用应该在此时能够继续播放视频才对,因此最好不要在Activity的onPause()方法中去处理播放器的暂停逻辑,而应该在onStop()方法中处理,并且在onStart()方法中恢复视频播放。另外,针对进入多窗口时活动会被重新重新创建,如果想改变这一默认行为,可以在AndroidManifest.xml中进行配置:

1
2
3
<activity
android:name=".MainActivity"
android:configChanges="origintation|keyboardHidden|screenSize|screenLayout"

这样不管进入多窗口模式还是横竖屏切换,Activity都不会被重新创建,而是会将屏幕发生变化的事件通知到Activity的onConfigurationChanged()方法中,因此,如果有这方面的需求,只需要重写onConfigurationChanged()即可。

当然,如果想禁用多窗口模式,只需要在 AndroidManifest.xml 中的 或者 标签中加入如下属性即可

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

需要注意的是,这个属性只有在项目的targetSdkVersion指定成24或者更高的时候才会有用。不过Android规定,如果targetSdkVersion小于24,并且Activity不允许横竖屏切换,那么应用也将不支持多窗口模式,如果不允许应用横竖屏切换,只需要在AndroidManifest.xml中添加如下配置:

1
android:screenOrientation=["portrait" | "landscape"]
谢谢你的鼓励