第十三章主要讲了一些日常开发中常用的技巧
一、全局获取 Context 的技巧
截止到目前,我们还没有碰见过为得不到 Context 而发愁的情况,因为我们很多的操作都是在活动中进行的,而活动本身就是一个 Context 对象,但是,当应用程序的架构逐渐复杂起来的时候,很多逻辑代码都将脱离 Activity 类,但此时你又恰恰需要使用 Context,这就伤脑筋了。
举个例子,你写了一个 Util 类,该类的某个方法需要用到 Context,怎么办?也许你会想到通过方法传递进来,还是用代码来说明吧。
public class Util {
public static void needContext(){
//下面的逻辑处理需要用到 Context 对象
}
}
大多数人的第一反应就是通过方法的参数传递进来
public class Util {
public static void needContext(Context context){
//下面的逻辑处理需要用到 Context 对象
}
}
是的,没问题,可以这样写,但是如果调用方也获取不到 Context 怎么办?我们可以使用另一种更好的方法来解决这个问题。
Android 提供了一个 Application 类,每当应用程序启动的时候,系统就会自动将这个类进行初始化。我们也可以定制一个自己的 Application 类,以便于管理程序内一些全局的状态信息,比如全局的 Context.
首先你需要新建一个类,继承自 Application
/**
* 自定义 Application
*/
public class MyApplication extends Application {
private static Context mContext;
/**
* 通过静态方法获取 Context
*/
public static Context getContext(){
return mContext;
}
/**
* 重写 onCreate() 方法
*/
@Override
public void onCreate() {
super.onCreate();
mContext = getApplicationContext();
}
}
接下来我们需要告知系统,当程序启动的时候应该初始化 MyApplication 类,而不是默认的 Application 类。
<!-- 这里注意,一定是全包名,或者前面有个" . ",否则系统找不到这个类 -->
<application
android:name=".MyApplication"
...>
...
</application>
接下来,我们就可以在 Util 类里面这样获取 Context 了。
public class Util {
public static void needContext(){
//下面的逻辑处理需要用到 Context 对象
Context context = MyApplication.getContext();
}
}
二、使用 Intent 传递对象
我们可以借助 Intent 的 putExtra() 方法来传递数据,里面可以传递boolean、byte、short、int、long、float、double、char、String,以及前面这些数据类型所对应的数组形式的数据类型,那么传递对象呢?
putExtra() 方法还有两个数据类型:Serializable 和 Parcelable
- Serializable 方式:
Serializable 是序列化的意思,表示将一个对象转换成可存储或可传输的状态。序列化后的对象可以在网络上进行传输,也可以存储到本地。至于序列化的方法也很简单,只需要让类实现 Serializable 接口即可。
- Serializable 方式:
/**
* 实体类
*/
public class Fruit implements Serializable{
...
}
使用 Intent 传递对象
//发送方
Fruit fruit = new Fruit();
fruit.setName("苹果");
Intent intent = new Intent(this,MainActivity.class);
intent.putExtra("fruit_data",fruit);
startActivity(intent);
//接收方
/*
通过 getSerializable() 方法获取序列化对象,再向下转型成 Fruit 对象即可
*/
Fruit fruit_data = (Fruit) getIntent().getSerializableExtra("fruit_data");
- Parcelable 方式:
不同于 Serializable ,Parcelable 方式的实现原理是将一个完整的对象进行分解,而分解后的每一部分都是 Intent 所支持的数据类型,这样就实现传递对象的功能了。
实现 Parcelable 的方式首先也是让一个类去实现 Parcelable 接口。
- Parcelable 方式:
/**
* 实体类
*/
public class Fruit implements Parcelable {
private String name;//水果的名字
private int imageId;//水果对应资源的图片id
public Fruit() {
}
public Fruit(String name, int imageId) {
this.name = name;
this.imageId = imageId;
}
protected Fruit(Parcel in) {
/*
这里注意,读取的顺序一定要和刚才写入的顺序一致。
*/
name = in.readString();
imageId = in.readInt();
}
/**
* 必须重写的方法
*/
@Override
public int describeContents() {
//这里直接返回 0 就可以了
return 0;
}
/**
* 必须重写的方法
*/
@Override
public void writeToParcel(Parcel dest, int flags) {
/*
将字段一一写出
字符串调用 writeString(),整数调用 writeInt() 依此类对
*/
dest.writeString(name);
dest.writeInt(imageId);
}
/**
* 定义一个常量,创建 Parcelable.Creator 接口的实现,并且指定泛型为当前类,
*/
public static final Creator<Fruit> CREATOR = new Creator<Fruit>() {
@Override
public Fruit createFromParcel(Parcel in) {
return new Fruit(in);
}
@Override
public Fruit[] newArray(int size) {
return new Fruit[size];
}
};
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getImageId() {
return imageId;
}
public void setImageId(int imageId) {
this.imageId = imageId;
}
@Override
public String toString() {
return "Fruit{" +
"name='" + name + '\'' +
", imageId=" + imageId +
'}';
}
}
具体的使用方法和 Serializable 类似,只是在获取的时候,只需要把 getSerializableExtra() 方法换成 getParcelableExtra() 方法即可。
注意:Serializable 的方式较为简单,但由于会把整个对象进行序列化,因此效率会比 Parcelable 方式低一些,所以在通常情况下还是更加推荐使用 Parcelable 的方式来实现 Intent 传递对象的功能。
三、定制自己的日志工具
理想状态下,我们的日志打印是可以控制的,当程序处于开发阶段时就让日志打印出来,当程序上线了之后就把日志屏蔽掉。
/**
* 日志工具类
*/
public class LogUtil {
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;
/*
这里通过改变 level 的值来控制打印日志的行为,比如:
让 level 等于 VERBOSE 就是打印所有日志,让 level 等于
WARN 就是只打印警告以上级别的日志,让 level 等于
NOTHING 就是把所有日志都屏蔽掉。
*/
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.v(tag, msg);
}
}
public static void i(String tag, String msg) {
if (level <= INFO) {
Log.v(tag, msg);
}
}
public static void w(String tag, String msg) {
if (level <= WARN) {
Log.v(tag, msg);
}
}
public static void e(String tag, String msg) {
if (level <= ERROR) {
Log.v(tag, msg);
}
}
}
四、创建定时任务
Android 中定时任务一般有两种实现方式,一种是使用 Java API 里提供的 Timer 类,一种是使用 Android 的 Alarm 机制。在大多数情况下,这两种方式都能实现类似的效果,但是 Timer 有一个明显的短板,它并不太适用于那么些需要长期在后台运行的定时任务,为了让电池更加耐用,每种手机都会有自己的休眠策略,Android 手机会在长时间不操作的情况下自动让 CPU 进入休眠功状态,这就可能导致 Timer 中的定时任务无法正常运行。而 Alarm 则具有唤醒 CPU 的功能,它可以保证在大多数情况下需要执行定时任务的时候 CPU 都能正常工作,需要注意,这里唤醒 CPU 唤醒屏幕完全不是一个概念。
Alarm机制
主要是借助了 AlarmManager 类来实现,这个类 NotificationManager 有点类似。
/*
SystemClock.elapsedRealtime() 方法可以获取到系统开机至今所经历时间的毫秒数
System.currentTimeMillis() 方法获取到的是1970年1月1日0点至今所经时间的毫秒数
*/
long triggerAtTime = SystemClock.elapsedRealtime() + 10 * 1000;
AlarmManager manager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
/*
参1:用于指定AlarmManager的工作类型,有四种值可选:
1.ELAPSED_REALTIME :表示让定时任务的触发时间从系统开机开始算起,但不会唤醒CPU
2.ELAPSED_REALTIME_WAKEUP :表示让定时任务的触发时间从系统开机开始算起,但会唤醒CPU
3.RTC :表示让定时任务的触发时间从1970年1月1日0时开始算起,但不会唤醒CPU
4.RTC_WAKEUP :表示让定时任务的触发时间从1970年1月1日0时开始算起,但会唤醒CPU
*/
manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,triggerAtTime,pendingIntent);
如果我们需要实现一个长时间在后台定时运行的服务要怎么做?
public class LongRunningService extends Service {
public LongRunningService() {
}
@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 alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
int anHour = 60 * 60 * 1000;//一小时的毫秒数
//设置时间为一小时后执行
long triggerAtTime = SystemClock.elapsedRealtime() + anHour;
Intent i = new Intent(this,LongRunningService.class);
PendingIntent pendingIntent = PendingIntent.getService(this,0,i,0);
//执行定时任务,这样会使得 LongRunningService 的onStartCommand() 方法每隔一小时就执行一次
alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,triggerAtTime,pendingIntent);
return super.onStartCommand(intent, flags, startId);
}
}
注意:从Android 4.4 系统开始, Alarm 任务的触发时间将变得不准确,有可能会延迟一段时间后任务才能得到执行。这不是 bug,而是系统在耗电性方面进行的优化。系统会自动检测目前有多少 Alarm 任务存在,然后将触发时间相近的几个任务放在一起执行,这样可以大幅度减少 CPU 被唤醒的次数,从而有效延长电池的使用时间。
如果你要求 Alarm 任务的执行事件必须准确无误,就要使用 AlarmManager 的 setExact() 方法代替 set() 方法。
Doze 模式
Doze模式是Android6.0系统中新加入的模式,从而可以极大幅度地延长电池的使用寿命。
Doze模式就是当设备是 Android 6.0 及以上系统时,如果该设备未插接电源,处于静止状态(Android 7.0 中删除了这一条件),且屏幕关闭了一段时间后,就会进入到 Doze 模式,在 Doze 模式下,系统会对 CPU、网络、Alarm 等活动进行限制,从而延长电池的使用寿命。
当然,系统并不会一直处于 Doze 模式,而是会间歇性地退出 Doze 模式一小段时间,在这段时间中,应用就可以去完成他们的同步操作、Alarm任务
注意:随着设备进入 Doze 模式的时间越长,间歇性地退出 Doze 模式的时间间隔也会越长,因为如果设备长时间不使用的话,是没有必要频繁退出 Doze 模式来执行同步等操作的。其中 Doze 模式下会受到限制的功能:
1.网络访问被禁止
2.系统忽略唤醒CPU或屏幕操作
3.系统不在执行WIFI扫描
4.系统不再执行同步服务
5.Alarm任务将会在下次退出Doze模式的时候执行
注意最后一条,如果在 Doze 模式下,我们的任务将会变得不准时
如果你要求Alarm任务即使在Doze模式下也必须正常执行,Android还提供了解决方案。调用 AlarmManager 的setAndAllowWhileIdle() 或者 setExactAndAllowWhileIdle() 方法就能让定时任务即使在 Doze 模式下也能正常执行了,这两个方法的区别和 set()与setEsact()方法之间的区别是一样的。
五、多窗口模式编程
Android 7.0 系统中引入的特色功能,允许我们在同一个屏幕中同时打开两个应用程序。
如何进入多窗口模式?一般手机底部按键有三个,左边的是返回键,右边的是HOME键,右边的是Overview键,点一下Overview键,会打开一个最近访问过的活动或任务的列表界面。
image.png
那么我们可以通过两种方式进入多窗口模式:
- 1.在 Overview 列表界面长按任意一个活动的标题,将该活动拖动到屏幕突出显示的区域,则可以进入多窗口模式。
-
2.打开任意一个程序,长按 Overview 按钮,也可以进入多窗口模式(没有试出来)
image.png
另外,我们还可以旋转,使上下分屏变成左右分屏
image.png
如果想要退出多窗口模式,只要将屏幕中央的分割线向屏幕任意方向拖动到底即可。
多窗口模式下的生命周期
多窗口模式不会改变活动原有的生命周期,只会将正和用户交互的那个活动设置为运行状态,将另一个活动设置为暂停状态。
我们新建两个项目,并且将这两个项目的启动 Activity 的生命周期都进行打印。
先运行第一个项目
image.png
在运行第二个项目
image.png
接下来我们进入到多窗口模式
image.png
可以看到,第一个项目先销毁然后重新创建,因为进入多窗口模式后活动的大小发生了比较大的变化,默认是会重新创建活动的。由于我进入多窗口模式后又让该项目获取了焦点,所以多执行了一个onResume(),接下来将第二个项目从 Overview 列表中选中。
image.png
这时候只要我们点击任意一个项目,就会切换这两个项目的暂停还是活动的状态。
image.png
一个onPause,另一个onResume
注意:在多窗口模式下我们需要考虑一些关键性的点,比如说,在多窗口模式下,用户仍然可以看到处于暂停状态的应用,如果是播放器之类的应用,此时就应该能继续播放视频才对。因此,我们最好不要在活动的 onPause() 方法中去处理视频播放器的暂停逻辑,应该在 onStop() 方法中处理,并且在 onStart() 方法中恢复视频播放。
注意:如果想改变进入多窗口模式时活动会被重新创建这一默认行为,可以在清单文件中进行如下配置,加入了如下配置后,不管是进入多窗口模式,还是横竖屏切换,活动都不会被重新创建,而是会将屏幕发生变化的事件通知到 Activity 的 onConfigurationChanged() 方法当中
<activity android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|screenSize|screenLayout">
</activity>
禁用多窗口模式
如果你不希望应用能够在多窗口模式下运行,就可以将这个功能关闭掉,禁用多窗口模式的方法很简单,只需要在清单文件中的 <application> 或 <activity> 标签中加入如下属性即可:
android:resizeableActivity="false",其中如果传 true 表示支持多窗口模式,也是默认的情况,如果传入 false 表示不支持多窗口模式。
注意:虽然说 android:resizeableActivity 属性的用法很简单,但是他有一个很严重的问题,就是这个属性只有当项目的 targetSdkVersion 指定成 24 或更高的时候才会有用,否则这个属性是无效的。
解决方案:Android 规定,如果项目指定 targetSdkVersion 低于 24,并且活动是不允许横竖屏切换的,那么该应用也将不支持多窗口模式。
那么让应用不允许横竖屏切换,只需要在清单文件的 <activity> 标签中加入如下配置即可:android:screenOrientation="portrait" 或 android:screenOrientation="landscape",其中 portrait 表示活动只支持竖屏,landscape 表示活动只支持横屏。
六、Lambda表达式
Java 8 中引入了一些非常有特色的功能,如 Lambda 表达式、stream API、接口默认实现等等。其中,stream API 和接口默认实现等特性都只支持 Android 7.0 及以上的系统,而 Lambda 表达式最低兼容到 Android 2.3 系统,所以我们来学习一下 Lambda 表达式。
Lambda 表达式本质上是一个匿名方法,它既没有方法名,也没有访问修饰符和返回值类型,使用它来编写代码将会更加简洁,更加易读。
如果要在 Android 项目中使用 Labmda 表达式或者 Java 8中的其他新特性,需要在 app/build.gradle 中添加如下配置:
android {
...
defaultConfig {
...
jackOptions.enabled = true;
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
注意:如果你的项目中有 Android Library 模块,那么上述方式是不能引用成功的,我们首先要在最外层的 project build.gradle 中加入这句话
classpath 'me.tatarka:gradle-retrolambda:3.2.0'
然后在要使用 lambda 表达式的 Module 的 build.gradle 中加入如下代码
apply plugin: 'me.tatarka.retrolambda'
android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
写个简单的例子:
//开启线程传统写法
new Thread(new Runnable() {
@Override
public void run() {
//具体处理逻辑
}
}).start();
//开启线程 Lambda 表达式
new Thread(() -> {
//具体处理偶偶机
}).start();
能这样写是因为 Thread 类的构造函数接收的参数是一个 Runnable 接口,并且改接口中只有一个待实现的方法。
注意:凡是这种只有一个待实现方法的接口,都可以使用 Lambda 表达式的写法
//传统写法
Runnable runnable = new Runnable() {
@Override
public void run() {
//具体处理逻辑
}
};
//Lambda 写法
Runnable runnable1 = () -> {
//具体处理逻辑
};
接下来,我们自定义一个接口,再使用 Lambda 表达式的方式进行实现。
/**
* 定义一个接口
*/
public interface MyListener {
//注意,该方法有返回值,有参数
String doSomething(String a,int b);
}
//Lambda 表达式写法
MyListener myListneer2 = (String a,int b) -> {
return a + b;
};
/*
还可以精简成这样
这里,Java 会自动判断出 a 和 b 的数据类型
*/
MyListener myListener2 = (a, b) -> {
return a + b;
};
假如有一个方法,需要传入 MyListener 接口
public void hello(MyListener mListener) {
String a = "Hello Lambda";
int b = 1024;
String result = mListener.doSomething(a, b);
Log.d(TAG, "hello: " + result);
}
//传统写法
hello(new MyListener() {
@Override
public String doSomething(String a, int b) {
return a + b;
}
});
//Lambda 表达式
hello((a, b) -> {
return a + b;
});
Lambda表达式的基本用法已经掌握了,接下来看一看在 Android 中有哪些常用的功能是可以使用 Lambda 表达式替换的。
其实,只要符合接口中只有一个待实现方法这个规则的功能,都是可以使用 Lambda 表达式来编写的,除了开启子线程之外,还有点击事件之类的功能。
//传统写法
findViewById(R.id.tv).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//处理点击事件
}
});
//Lambda 表达式
findViewById(R.id.tv).setOnClickListener((v) -> {
//处理点击事件
});
注意:另外,当接口的待实现方法有且只有一个参数的时候,我们还可以进一步简化,将参数外面的括号去掉
//Lambda 表达式进一步简化
findViewById(R.id.tv).setOnClickListener(v -> {
//处理点击事件
});












网友评论