Android开发基础13-Android 中的一些高级技巧

本篇文章主要介绍以几下个知识点:

  • 获取全局 Context
  • 使用 Intent 传递对象
  • 定制日志工具
  • 创建定时任务
  • 多窗口模式编程

13.1 全局获取 Context 的技巧

  在某些情况下,获取 Context 并非是一件容易事,下面就来学习让你在项目的任何地方都能轻松获取到 Context 的一种技巧。

  Android 提供了一个 Application 类,每当应用程序启动时,系统就会自动将这个类进行初始化。我们可以定制一个自己的 Application 类,以便管理程序内一些全局的状态信息,比如全局的 Context。

  定制一个自己的 Application 并不复杂,首先需要创建一个 MyApplication 类继承自 Application,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyApplication extends Application {

private static Context context;

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

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

  接下来在 AndroidManifest.xml 文件的标签下进行指定就可以了,如下:

1
2
3
4
5
6
7
8
9
 <application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
. . .
</application>

  这样就实现了一种全局获取 Context 的机制,之后在项目的任何地方想要获取 Context,只需调用 MyApplication.getContext() 就可以了,如弹吐司:

1
Toast.makeText(MyApplication.getContext(),"提示内容",Toast.LENGTH_SHORT).show();

  任何一个项目都只能配置一个 Application,当引用第三方库如 LitePal 时要配置 LitePalApplication 就会起冲突了,这种情况就要在自己的 Application 中去调用 LitePal 的初始化方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyApplication extends Application {

private static Context context;

@Override
public void onCreate() {
context = getApplicationContext();
// 调用 LitePal 的初始化方法
LitePal.initialize(context);
}

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

  使用上面的写法,把 Context 对象通过参数传递给 LitePal,效果和在 AndroidManifest.xml 中配置 LitePalApplication 是一样的。

  当然,我个人是更习惯于通过获取全局类实例的方法来定制自己的 Application 类,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyApplication extends Application {

private static MyApplication mInstance;

@Override
public void onCreate() {
super.onCreate();
mInstance = this;
// 调用 LitePal 的初始化方法
LitePal.initialize(this);
}

/**
* Singleton main method. Provides the global static instance of the helper class.
* @return The MyApplication instance.
*/
public static synchronized MyApplication getInstance() {
return mInstance;
}
}

13.2 使用 Intent 传递对象

  使用 Intent 时,可以在 Intent 中添加一些附加数据,以达到传值的效果,如在第一个活动中添加如下代码:

1
2
3
4
Intent intent = new Intent(FirstActivity.this,SecondActivity.class);
intent.putExtra("string_data","hello");
intent.putExtra("int_data",100);
startActivity(intent);

  然后在第二个活动中就可以获得这些值了,如下:

1
2
getIntent().getStringExtra("string_data");
getIntent().getIntExtra("int_data",0);

  但上面的 putExtra() 方法中所支持的数据类型是有限的,若要传递一些自定义对象时就无从下手了,下面就来学习下用 Intent 来传递对象的技巧:SerializableParcelable

13.2.1 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
public class Person implements Serializable{

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

  接下来在第一个活动中的写法非常简单:

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

  然后在第二个活动中获取对象也非常简单:

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

  这样就实现了使用 Intent 传递对象了。

13.2.2 Parcelable 方式

  Parcelable 方式的实现原理是将一个完整的对象进行分解,而分解后的每一部分都是 Intent 所支持的数据类型,这样也就实现传递对象的功能了。

  Parcelable 的实现方式要稍微复杂一些,修改 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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class Person implements Parcelable {

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


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

@Override
public void writeToParcel(Parcel dest, int flags) {
// 写出数据:将 Person 类中的字段一一写出
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();//读取 age
return person;
}

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

  接下来在第一个活动中的写法不变,在第二个活动中获取对象稍加改动:

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

  这样就也实现使用 Intent 传递对象了。

  对比一下,Serializable 的方式较为简单,但由于把整个对象进行序列化,效率会比 Parcelable 方式低。一般推荐使用 Parcelable 的方式来实现 Intent 传递对象的功能。

13.3 定制自己的日志工具

  开发一个项目时,定制一个自己的日志工具能够自由的控制日志的打印,当程序处于开发阶段时让日志打印出来,当程序上线后把日志屏蔽。

  新建日志工具类 LogUtils 如下:

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
35
36
37
38
39
40
41
42
43
44
45
46
public class LogUtils {

public static final int VERBOSE = 1;

public static final int DEBUG = 2;

public static final int INFO = 3;

public static final int WARN = 4;

public static final int ERROR = 5;

public static final int NOTHING = 6;

public static int level = VERBOSE;

public static void v(String tag,String msg){
if (level <= VERBOSE){
Log.v(tag,msg);
}
}

public static void d(String tag,String msg){
if (level <= DEBUG){
Log.d(tag,msg);
}
}

public static void i(String tag,String msg){
if (level <= INFO){
Log.i(tag,msg);
}
}

public static void w(String tag,String msg){
if (level <= WARN){
Log.w(tag,msg);
}
}

public static void e(String tag,String msg){
if (level <= ERROR){
Log.e(tag,msg);
}
}
}

  上述代码提供了5个自定义的日志方法,其内部分别调用了 Android 自带的打印日志方法,在项目里使用就像使用普通日志工具一样,如打印一行 DEBUG 级别的日志可以这样写:

1
LogUtils.d("TAG","debug log");

  值得注意的是,LogUtils 定义了一个静态变量 level,在开发阶段将 level 指定成 VERBOSE,当项目正式上线时将 level 指定成 NOTHING,将所有日志屏蔽。

13.4 创建定时任务

  Android 中的定时任务一般有两种实现方式,一种是使用 Java API 里提供的 Timer 类(不太适用于需要长期在后台运行的定时任务),一种是使用 Android 的 Alarm 机制(具有唤醒 CPU 功能,可以保证大多数情况下执行定时任务时 CPU 能正常工作)。

13.4.1 Alarm 机制

  Alarm 机制的用法不复杂,主要是借助 AlarmManager 类来实现的。比如想要设定一个任务在 10 秒钟后执行,可写成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 获取 AlarmManager 的实例
AlarmManager manager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);

// 设置触发时间
// SystemClock.elapsedRealtime() 获取系统开机至今所经历时间的毫秒数
// SystemClock.currentTimeMillis() 获取1970年1月1日0点至今所经历时间的毫秒数
long triggerAtTime = SystemClock.elapsedRealtime() + 10 * 1000;

// 3个参数:指定 AlarmManager 的工作类型、定时任务的触发时间、PendingIntent
// 其中AlarmManager 的工作类型有四种:
// ELAPSED_REALTIME 定时任务的触发时间从系统开机开始时算起,不会唤醒 CPU
// ELAPSED_REALTIME_WAKEUP 系统开机开始时算起,会唤醒 CPU
// RTC 从1970年1月1日0点开始算起,不会唤醒 CPU
// RTC_WAKEUP 从1970年1月1日0点开始算起,会唤醒 CPU
manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,triggerAtTime,pandingIntent);

  举个例子,实现一个长时间在后台定时运行的服务,首先新建一个普通的服务 LongRunningService,将触发定时任务的代码写到 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
public class LongRunningService extends Service {
public LongRunningService() {
}

@Override
public IBinder onBind(Intent intent) {
throw new UnsupportedOperationException("Not yet implemented");
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
new Thread(new Runnable() {
@Override
public void run() {
// 在这里执行具体的逻辑操作
}
}).start();
AlarmManager manager = (AlarmManager) getSystemService(ALARM_SERVICE);
int anHour = 60 * 60 * 1000;//1小时的毫秒数
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);
}
}

  然后,要启动定时服务时调用如下代码即可:

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

  值得注意的是,从 Android 4.4开始,由于系统在耗电方面的优化,Alarm 任务的触发时间变得不准确,可能会延迟一段时间后再执行。当然,使用 AlarmManager 的 setExact() 方法来替代 set() 方法,基本上可以保证任务准时执行。

13.4.2 Doze 模式

  在 Android 6.0中,谷歌加入了一个全新的 Doze 模式,可以极大幅度地延长电池的使用寿命。下面就来了解下这个模式,掌握一些编程时注意事项。

  在 6.0 及以上系统的设备上,若未插接电源,处于静止状态(7.0中删除了这一条件),且屏幕关闭了一段时间之后,就会进入到 Doze 模式。在 Doze 模式下,系统会对 CPU、网络、Alarm 等活动进行限制,从而延长电池的使用寿命。

  当然,系统不会一直处于 Doze 模式,而是会间歇性的退出一小段时间,在这段时间应用可以去完成它们的同步操作、Alarm 任务等,其工作过程如下:

  Doze 模式下受限的功能有:
 (1)网络访问被禁止
 (2)系统忽略唤醒CPU或屏幕操作
 (3)系统不再执行WIFI扫描
 (4)系统不再执行同步任务
 (5)Alarm 任务将会在下次退出 Doze 模式时执行

  特殊需求,要 Alarm 任务在 Doze 模式下也必须正常执行,则可以调用 AlarmManager 的 setAndAllowWhileIdle() 或 setExactAndAllowWhileIdle() 方法。

13.5 多窗口模式编程

  Android 7.0中引入了一个非常有特色的功能——多窗口模式,允许在同一个屏幕中同时打开两个应用程序。

13.5.1 进入多窗口模式

  手机的导航栏上有3个按钮:左边 Back 按钮、中间 Home 按钮、右边 OverView 按钮,如下所示:

  OverView 按钮的作用是打开一个最近访问过的活动或任务的列表界面,进入多窗口模式需要用到 OverView 按钮,并且有两种方式:

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

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

  多窗口模式效果如下:


  可以看出多窗口模式下,应用界面缩小很多,编写程序时要多考虑使用 match_parent 属性、RecyclerView、ScrollView 等控件,适配各种不同尺寸的屏幕。

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

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

  进入多窗口模式时活动会被重新创建,若要改变这一默认行为,可以在 AndroidManifest.xml 中对活动添加如下配置:

1
android:configChanges="orientation|keyboardHidden|screenSize|screenLayout"

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

13.5.3 禁用多窗口模式

  禁用多窗口模式的方法很简单,只需在 AndroidManifest.xml 的标签中加入如下属性即可:

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

  其中,true 表示支持多窗口模式,false 表示不支持,若不配置这属性默认是 true。

  虽说 android:resizeableActivity 这个属性的用法简单,但这个属性只适用于 targetSdkVersion 24 或更高的时候,若低于24则无效,可能会被告知此应用在多窗口模式下可能无法正常工作,并进入多窗口模式。

  Android 规定,若项目指定的 targetSdkVersion 低于24,并且活动是不允许横竖屏切换时是不支持多窗口模式的。因此针对上面的情况,就需要在 AndroidManifest.xml 的标签中配置如下属性:

1
android:screenOrientation=["portrait"|"landscape"]

  其中,portrait 表示只支持竖屏,landscape 表示只支持横屏。

  本篇文章就介绍到这。

Jason Xu wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
坚持原创技术分享,您的支持将鼓励我继续创作!