一, 全局获取 Context 的技巧
前面我们很多地方都使用到了 Context, 弹出 Toast 的时候, 启动活动的时候, 发送广播的时候, 操作数据库的时候, 使用通知的时候等等. 或许目前来说我们并没有为得不到 Context 而发愁, 因为我们很多地方都是在活动中进行的, 而活动本身就是一个 Context 对象, 但是, 当应用程序的架构逐渐开始复杂起来的时候, 很多的逻辑代码都将脱离 Activity 类, 但此时又恰恰需要使用 Context, 特许这个时候就会感到有些伤脑筋了.
举个例子, 在前面网络编程的最佳实践中, 我们编写了一个 HttpUtil 类, 在这里将一些通用的网络操作封装了起来, 代码如下:
这里使用 sendHttpRequest() 方法来发送 HTTP 请求显然是没有问题的, 并且我们还可以在回调方法中处理服务器返回的数据, 但现在我们想对 sendHttpRequest() 方法进行一些优化, 当检测到网络不存在的时候就给用户一个 Toast 提示, 并且不再执行后面的代码, 看似一个简单的功能, 可是却存在一个让人头疼的问题, 弹出 Toast 提示需要一个 Context 参数, 而我们在 HttpUtil 类中显然是获取不到 Context 对象的, 这个时候应该怎么办呢?
其实要想快速解决这个问题也很简单, 大不了在 sendHttpRequest() 方法中添加一个 Context 参数就行了, 于是将 HttpUtil 中的代码进行如下修改:
可以看到, 这里在方法中添加了一个 Context 参数, 并且假设有一个 isNetworkAvailable() 方法用于判断当前网络是否可用, 如果网络不可用的话, 就弹出一个 Toast 提示, 并将方法 return 掉. 虽说这也是一种解决方案, 但是却有点推卸责任的嫌疑, 因为我们将获取 Context 的任务转移给了 sendHttpRequest() 方法的调用方, 至于调用方能不能得到 Context 对象, 那就不是我们需要考虑的问题了.
由此看出, 在某些情况下, 获取 Context 并非是那么容易的一件事, 有时候挺伤脑筋的. 所以接下来就来学习一个技巧, 能够在项目的任何地方都能够轻松获取到 Context.
Android 提供了一个 Application 类, 每当应用程序启动的时候, 系统就会自动将这个类进行初始化, 而我们可以定制一个自己的 Application 类, 以便于管理程序内一些全局的状态信息, 比如说全局 Context.
定制一个自己的 Application 其实并不复杂, 首先我们需要创建一个 MyApplication 类继承自 Application, 代码如下:
可以看到, MyApplication 中的代码非常简单, 这里我们重写了父类的 onCreate() 方法, 并通过调用 getApplicationContext() 方法得到了一个应用程序级别的 Context, 然后又提供了一个静态的 getContext() 方法, 在这里将刚才获取到的 Context 进行返回.
接下来需要告知系统, 当程序启动的时候应该初始化 MyApplication 类, 而不是默认的 Application 类, 这一步也很简单, 在 AndroidManifest 中的 < application > 标签下进行指定就可以了, 代码如下所示:
注意这里指定 MyApplication 的时候一定要加上完整的包名, 不然系统将无法找到这个类, 这里前面的点号就代表完整的包名.
这样我们就已经实现了一种全局获取 Context 的机制, 之后不管你想在项目的任何地方使用 Context, 只需要调用一下 MyApplication.getContext() 就可以了.
那么接下来我们再对 sendHttpRequest() 方法进行优化, 代码如下:
可以看到, sendHttpRequest() 方法不需要在通过传参的方式来得到 Context 对象, 而是调用一下 MyApplication.getContext() 方法就可以了, 有了这个技巧, 你再也不用为得不到 Context 对象而发愁了.
然后再回顾在使用 LitePal 操作数据库的时候, 当时为了让 LitePal 正常工作, 要求必须在 AndroidManifest 中配置如下内容:
其实道理也是一样的, 因为经过这样的配置之后, LitePal 就能在内部自动获取到 Context 了, 不过这里有个疑问, 就是如果我们已经配置过了自己的 Application 怎么办? 这样岂不是和 LitePalApplication 冲突了? 没错, 任何一个项目都只能配置一个 Application, 对于这种情况, LitePal 提供了很简单的解决方案, 那就是在我们自己的 Application 中去调用 LitePal 的初始化方法就可以了, 如下所示:
使用这种写法, 就相当于我们把全局的 Context 对象通过参数传递给了 LitePal, 效果和在 AndroidManifest 中配置 LitePalApplication 是一模一样的.
二, 使用 Intent 传递对象
Intent 的用法相信已经比较熟悉了, 我们可以借助它来启动活动, 发送广播, 启动服务等. 在进行上述操作的时候, 我们还可以在 Intent 中添加一些附加数据, 以达到传值的效果, 比如在 FirstActivity 中添加如下代码:
这里调用了 Intent 的 putExtra() 方法来添加要传递的数据, 之后在 MainActivity 中就可以得到这些值了, 代码如下:
但是这里我们发现, putExtra() 方法中所支持的数据类型是有限的, 虽然常用的一些数据类型它都会支持, 但是当你想去传递一些自定义对象的时候, 就会发现无从下手. 那么接下来我们就来学习一下使用 Intent 来传递对象的技巧.
2.1,Serializable 方式
使用 Intent 来传递对象通常有两种实现方式: Serializable 和 Parcelable. 先来看第一种方式:
Serializable 是序列化的意思, 表示将一个对象转换成可存储或可传输的状态, 序列化后的对象可以在网络上进行传输, 也可以存储到本地. 至于序列化的方法也很简单, 只需要让一个类去实现 Serializable 这个接口就可以了.
比如说有一个 Person 类, 其中包含了 name 和 age 这两个字段, 想要将它序列化就可以这样写:
其中, get,set 方法都是用于赋值和读取字段数据的, 最重要的部分是在第一行, 这里让 Person 类去实现了 Serializable 接口, 这样所有的 Person 对象就都是可序列化的了.
接下来在 FirstActivity 中想写法非常简单:
可以看到, 这里我们创建了一个 Person 的实例, 然后就直接将它传入到 putExtra() 方法中了, 由于 Person 类实现了 Serializable 接口, 所以才可以这样写.
接下来在 MainActivity 中获取这个对象也很简单, 写法如下:
这里调用了 getSerializableExtra() 方法来获取通过参数传递过来的序列化对象, 接着再将它向下转型成 Person 对象, 这样我们就成功实现了使用 Intent 来传递对象的功能了.
2.2,Parcelable 方式
除了 Serialiable 之外, 使用 Parcelable 也可以实现相同的效果, 不过不同于将对象进行序列化, Parcelable 方式的实现原理是将一个完整的对象进行分解, 而分解后的每一部分都是 Intent 所支持的数据类型, 这样也就实现传递对象的功能了.
下面来看一下 Parcelable 的实现方式, 修改 Person 中的代码:
Parcelable 的实现方式要稍微复杂一些, 可以看到, 首先我们让 Person 类去实现了 Parcelable 接口, 这样就必须要重写 describeContents() 和 writeToParcel() 这两个方法, 其中 describeContents() 方法直接返回 0 就可以了, 而 writeToParcel() 方法中我们需要调用 Parcel 的 writeXxx() 方法, 将 Person 类中的字段一一写出. 注意, 字符串型数据就调用 writeString() 方法, 整型数据就调用 writeInt() 方法, 以此类推.
除此之外, 我们还必须在 Person 类中提供一个名为 CREATE 的常量, 这里创建了 Parcelable.Creator 接口的一个实现, 并将泛型指定为 Person, 接着需要重写 createFromParcel() 和 newArray() 这两个方法, 在 createFromParcel() 方法中我们要去读写刚才写出的 name 和 age 字段, 并创建一个 Person 对象进行返回, 其中 name 和 age 都是调用 Parcel 的 readXxx() 方法读取到的, 注意这里读取的顺序一定要和刚才写出的顺序完全相同. 而 newArray() 方法中的实现就简单多了, 只需要 new 出一个 Person 数组, 并使用方法中传入的 i 作为数组大小就可以了.
接下来, 在 FirstActivity 中我们仍然可以使用相同的代码来传递 Person 对象, 只不过在 MainActivity 中获取对象的时候需要稍加改动, 如下所示:
注意: 这里不再是调用 getSerializableExtra() 方法, 而是调用 getParcelableExtra() 方法来获取传递过来的对象, 其他的地方都完全相同.
这样我们就把使用 Intent 来传递对象的两种方式都学习完了, 对比一下, Serializable 的方式较为简单, 但由于会把整个对象进行序列化, 因此效率会比 Parcelable 方式低一些, 所以在通常情况下还是更加推荐使用 Parcelable 的方式来实现 Intent 传递对象的功能.
三, 定制自己的日志工具
在刚开始的时候就已经学过了 Android 日志工具的用法, 并且日志工具也确实贯穿了整个学习过程, 基本上每一章都有用过, 虽然 Android 中自带的日志工具功能非常强大, 但也不能说是完全没有缺点, 例如打印日志的控制方面就做的不是很好. 打个比方, 你正在编写一个比较庞大的项目, 期间为了方便调试, 在代码的很多地方都打印了大量的日志, 最近项目已经基本完成了, 但是却有一个非常让人头疼的问题, 之前用于调式的那些日志, 在项目正式上线之后仍然会照常打印, 这样不仅会降低程序的运行效率, 还有可能将一些机密性的数据泄漏出去.
那么该怎么办呢? 难道要一行一行地把所有打印日志的代码都删掉? 显然这不是什么好点子, 不仅费时费力, 而且以后你继续维护这个项目的时候可能还会需要这些日志, 因此最理想的情况是能够自由地控制日志的打印, 当程序处于开发阶段时就让日志打印出来, 当程序上线了之后就把日志屏蔽掉.
看起来好像是挺高级的一个功能, 其实并不复杂, 我们只需要定制一个自己的日志工具就可以轻松完成了, 比如新建一个 LogUtil 类, 代码如下:
可以看到, 我们在 LogUtil 中先定义了 VERBOSE,DEBUG,INFO,WARN,ERROR,NOTHING 这 6 个整型常量, 并且它们对应的值都是递增的, 然后又定义了一个静态变量 level, 可以将它的值指定为上面 6 个常量中的任意一个.
接下来提供了 v(),d(),i(),w(),e() 这 5 个自定义的日志方法, 在其内部分别调用了 Log.v(),Log.d(),Log.i(),Log.w(),Log.e() 这 5 个方法来打印日志, 只不过在这些自定义的方法中我们加入了一个 if 判断, 只有当 Level 的值小于或等于对应日志级别值的时候才会将日志打印出来.
这样就把一个自定义的日志工具创建好了, 之后在项目里我们可以像使用普通的日志工具一样使用 LogUtil, 比如打印一行 DEBUG 级别的日志, 即可以这样写:
打印一行 WARN 级别的日志就可以这样写:
然后我们只需要修改 level 变量的值, 就可以自由的控制日志的打印行为了, 比如让 level 等于 VERBOSE 就可以把所有日志都打印出来, 让 level 等于 WARN 就可以打印警告以上级别的日志, 让 Level 等于 NOTHING 就可以把所有日志都屏蔽掉.
使用了这种方法之后, 刚才说的那个问题就不复存在了, 只需要在开发阶段将 level 指定成 VERBOSE, 当项目正式上线的时候将 level 指定成 NOTHING 就可以了.
四, 调试 Android 程序
当开发过程中遇到一些很奇怪的 bug, 但又迟迟定位不出来是什么原因的时候, 最好的解决办法就是调试了, 调试允许我们逐行地执行代码, 并可以实时观察内存中的数据, 从而能够比较轻松的查出问题的原因. 这里就来学习一下使用 Android Studio 来调试 Android 程序的技巧.
在前面广播机制的最佳实践中, 我们编写了一个强制下线的程序, 这里我们就通过这个例子来学习 Android 程序的调试方法, 这个程序中有一个登录功能, 比如说现在登录出现了问题, 我们就可以通过调试来定位问题的原因.
调试工作的第一步是添加断点, 这里由于我们要调试登录部分的问题, 所以断点可以加在登录按钮的点击事件里面, 添加断点的方法很简单, 只需要在相应代码的左边点击一下就可以了. 如下所示:
如果想要取消这个断点, 对着它再次点击就可以了.
添加好了断点, 接下来就可以对程序进行调试了, 点击 Android Studio 顶部工具栏中的 Debug 按钮, 如下所示, 就会使用调试模式来启动程序了:
等到程序运行起来的时候, 首先会看到一个提示框, 如图所示:
这个框很快会自动消失, 然后在输入框里输入账号和密码, 并点击 Login 按钮, 这时 Android Studio 就会自动打开 Debug 窗口, 如图所示:
接下来每按一次 F8 键, 或者是下面红色圆圈中的按钮, 代码就会向下执行一行, 并且通过 Variables 视图还可以看到内存中的数据:
可以看到, 我们从输入框里获取到的账号密码分别是 adc 和 123, 而程序里要求正确的账号密码是 admin 和 123456, 所以登录才会出现问题, 这样我们就通过调试的方式轻松地把问题定位出来了, 调试完成之后点击 Debug 窗口中的 Stop 按钮, 如下所示, 来结束调试即可:
这种调试方式虽然完全可以正常工作, 但是在调试模式下, 程序的运行效率将会大大地降低, 如果你的端点加在一个比较靠后的位置, 需要执行很多操作才能运行到这个断点, 那么前面这些操作就都会有一些卡顿的感觉. 为了解决这个问题, Android 还提供了另外一种调试的方式, 可以让程序随时进入到调试模式, 下面我们就来尝试一下.
这次不需要选择调试模式来启动程序了, 就使用正常的方式来启动程序, 由于现在不是在调试模式下, 程序的运行速度比较快, 可以把账号和密码输入好, 然后点击 Android Studio 顶部工具栏的 Attach debugger to Android process 按钮, 如图所示:
此时会弹出一个进程选择提示框:
这里目前只列出了一个进程, 也就是我们当前程序的进程, 选中这个进程点击 OK 按钮, 就会让这个进程进入到调试模式了. 接下来在程序中点击 Login 按钮, Android Studio 同样也会自动打开 Debug 窗口, 之后的流程都是相同的了, 相比起来, 第二个调试方式比第一种更加灵活, 也更加常用.
五, 创建定时任务
Android 中的定时任务一般有两种实现方式, 一种是使用 Java API 里提供的 Timer 类, 一种是使用 Android 的 Alarm 机制, 这两种方式在多数情况下都能实现类似的效果, 但 Timer 有一个明显的短板, 它并不太适用于那些需要长期在后台运行的定时任务, 我们都知道, 为了能让电池更加耐用, 每种手机都会有自己的休眠策略, Android 手机就会在很长时间不操作的情况下自动让 CPU 进入到睡眠状态, 这就有可能导致 Timer 中的定时任务无法正常运行. 而 Alarm 则具有唤醒 CPU 的功能, 它可以保证在大多是情况下需要执行定时任务的时候 CPU 都能正常工作, 需要注意, 这里唤醒 CPU 和唤醒屏幕完全不是一个概念, 千万不要产生混淆.
5.1,Alarm 机制
首先来看一下 Alarm 机制的用法, 其实并不复杂, 主要是借助了 AlarmManager 类来实现的, 这个类和 NotificationManager 有点类似, 都是通过调用 Context 的 getSystemService() 方法来获取实例的, 只是这里需要传入的参数是 Context.ALARM_SERVICE, 因此, 获取一个 AlarmManager 的实例就可以写成:
接下来调用 AlarmManager 的 set() 方法就可以设置一个定时任务了, 比如说想要设定一个任务在 10 秒钟后执行, 就可以写成:
上面两行代码可能不一定能看明白, 因为 set() 方法中需要传入的 3 个参数稍微有点复杂, 下面就来详细的分析一下: 第一个参数是一个整型参数, 用于指定 AlarmManager 的工作类型, 有 4 种值可选, 分别是:
使用 SystemClock.elapsedRealtime() 方法可以获取到系统开机至今所经历时间的毫秒数, 使用 System.currentTimeMillis() 方法可以获取到 1970 年 1 月 1 日 0 点至今所经历时间的毫米数.
然后看一下第二个参数, 这个参数就好理解了, 就是定时任务触发时间, 以毫秒为单位. 如果第一个参数使用的是 ELAPSED_REALTIME 或 ELAPSED_REALTIME_WAKEUP, 则这里传入开机至今的时间再加上延迟执行的时间. 如果第一个参数使用的是 RTC 或 RTC_WAKEUP, 则这里传入 1970 年 1 月 1 日 0 点至今的时间再加上延迟执行的时间.
第三个参数是一个 PendingIntent, 对于它应该不会陌生了, 这里我们一般会调用 getService() 方法或者 getBroadcast() 方法来获取一个能够执行服务或广播的 PendingIntent, 这样当定时任务被触发的时候, 服务的 onStartCommand() 方法或广播接收器的 onReceive() 方法就可以得到执行.
了解了 set() 方法的每个参数之后, 就能想到, 设定一个任务在 10 秒钟后执行也可以写成:
那么, 如果我们要实现一个长时间在后台定时运行的服务该怎么做呢? 其实很简单, 首先新建一个普通的服务, 比如把它起名叫 LongRunningService, 然后将出发定时任务的代码写到 onStartCommand() 方法中, 如下所示:
可以看到, 这里先是在 onStartCommand() 方法中开启了一个子线程, 这样就可以在这里执行具体的逻辑操作了. 之所以要在子线程里执行逻辑操作, 是因为逻辑操作也是耗时的, 如果放在主线程里执行可能会对定时任务的准确性造成轻微的影响.
创建线程之后的代码就是我们刚刚讲解的 Alarm 机制的用法了, 先是获取到了 AlarmManager 的实例, 然后定时任务的触发时间为一小时后, 再使用 PendingIntent 指定处理定时任务的服务为 LongRunningService, 最后调用 set() 方法完成设定.
这样我们就将一个长时间在后台定时运行的服务成功实现了. 因为一旦启动了 LongRunningService, 就会在 onStartCommand() 方法里设定一个定时任务, 这样一小时后将会再次启动 LongRunningService, 从而也就形成了一个永久的循环, 保证 LongRunningService 的 onStartCommand() 方法可以每隔一小时就执行一次.
最后只需要在你想要启动定时服务的时候调用如下代码即可:
另外需要注意的是, 从 Android4.4 系统开始, Alarm 任务的触发时间将会变得不准确, 有可能会延迟一段时间后任务才能得到执行. 这并不是个 bug, 而是系统在耗电性方面进行的优化. 系统会自动检测目前有多少 Alarm 任务存在, 然后将触发时间相近的几个任务放在一起执行, 这就可以大幅度地减少 CPU 被唤醒的次数, 从而有效延长电池的使用时间.
当然, 如果要求 Alarm 任务的执行时间必须准确无误, Android 仍然提供了解决方案, 使用 AlarmManager 的 setExtra() 方法来代替 set() 方法, 就基本上保证了任务能够准时的执行了.
5.2,Doze 模式
虽然 Android 的每个系统版本都在手机电量方面努力进行优化, 不过一直没能解决后台服务泛滥, 手机电量消耗过快的问题. 于是 Android6.0 系统中, 谷歌加入了一个全新的 Doze 模式, 从而可以极大幅度地延长电池的使用寿命.
首先看一下到底什么是 Doze 模式, 当用户的设备是 Android6.0 或以上系统时, 如果该设备未插电源, 处于静止状态 (Android7.0 中删除了这一条件), 且屏幕关闭了一段时间之后, 就会进入到 Doze 模式. 在 Doze 模式下, 系统会对 CPU, 网络, Alarm 等活动进行限制, 从而延长了电池的使用寿命.
当然, 系统并不会一直处于 Doze 模式, 而是会间歇性地退出 Doze 模式一小段时间, 在这段时间中, 应用就可以去完成它们的同步操作, Alarm 任务等等, 下图完整描述了 Doze 模式的工作过程.
可以看到, 随着设备进入 Doze 模式的时间越长, 间歇性地退出 Doze 模式的时间间隔也会越长. 因为如果设备长时间不使用的话, 是没必要频繁退出 Doze 模式来执行同步等操作的, Android 在这些细节上的把控使得电池的寿命进一步得到了延长.
接下来就具体来看一下 Doze 模式下有哪些功能受到限制吧:
1. 网络访问被禁止
2. 系统忽略唤醒 CPU 或者屏幕操作
3. 系统不再这些 Wi-Fi 扫描
4. 系统不再执行同步服务
5.Alarm 任务将会在下次退出 Doze 模式的时候执行
注意最后一条, 也就是说, 在 Doze 模式下, 我们的 Alarm 任务将会变得不准时, 当然, 这在大多数情况下都是合理的, 因为只有当用户长时间不使用手机的时候才会进入 Doze 模式, 通常在这种情况下对 Alarm 任务的准时性要求并没有那么高.
不过, 如果真的非常特殊的需求, 要求 Alarm 任务即使在 Doze 模式下也必须正常执行, Android 还是提供了解决方案, 调用 AlarmManager 的 setAndAllowWhileIdle() 或 setExtraAndAllowWhileIdle() 方法就能让定时任务即使在 Doze 模式下也能正常执行了, 这两个方法之间的区别和 set(),setExtra() 方法之间的区别是一样的.
六, 多窗口模式编程
由于手机屏幕大小的限制, 传统情况下一个手机只能同时打开一个应用程序, 无论是 Android,iOS 还是 Windows Phone 都是如此. 我们也早就习惯了, 认为这是理所应当的事情. 而 Android7.0 系统中却引入了一个非常有特色的功能 -- 多窗口模式, 它允许我们在同一个屏幕中同时打开两个应用程序, 对于手机屏幕越来越大的今天, 这个功能确实是越发重要了, 所以接下来就针对这一主题进行学习.
6.1, 进入多窗口模式
首先需要知道, 我们不用编写任何额外的代码来让应用程序支持多窗口模式, 事实上, 前面所编写的所以项目都是支持多窗口模式的, 但是这并不意味着我们就不需要对多窗口模式进行学习, 因为系统化地了解这些知识点才能编写出在多窗口模式下的兼容性更好的程序.
先来看一下如何进入到多窗口模式, 手机的导航栏肯定大家都见过, 上面有三个按钮: 其中左边的 Back 按钮和中间的 Home 按钮我们都经常使用, 但是右边的 Overview 按钮使用得就比较少了. 这个按钮的作用是打开一个最近访问过的活动或任务的列表界面, 从而能够方便地在多个应用程序之间进行切换. 如下所示:
我们可以通过以下两种方式进入多窗口模式:
1. 在 Overview 列表界面上长按任意一个活动的标题, 将该活动拖动到屏幕突出显示的区域, 则可以进入多窗口模式.
2. 打开任意一个程序, 长按 Overview 按钮, 也可以进入多窗口模式.
比如说我们首先打开了 Email 按钮和 Gallery 按钮, 然后长按 Overview 按钮, 效果如下所示:
可以看到现在整个屏幕被分成了上下两个部分, Gallery 程序占据了上半屏, 下半屏被 Email 程序占据, 我们还可以将模拟器旋转至水平方向, 这样上下分屏的多窗口模式会自动切换成左右分屏的多窗口模式.
多窗口模式的用法大概就是这个样子的, 我们可以将任意两个应用程序同时打开, 这样就能组合出许多更为丰富的使用场景, 比如说刷微博的同时还能时刻关注 QQ 消息, 看电影的同时还能和别人一直聊着微信等等. 如果想要退出多窗口模式, 只需要再次长按 Overview 按钮, 或者将屏幕中央的分隔线向屏幕任意一个方向拖动到底即可.
可以看出, 在多窗口模式下, 整个应用的界面会缩小很多, 那么编写程序时就应该多考虑使用 match_parent 属性, RecyclerView,ListView,ScrollView 等控件, 来让应用的界面能够更好地适配各种不同尺寸的屏幕, 尽量不要出现屏幕尺寸变化过大时界面无法显示的情况.
6.2, 多窗口模式下的生命周期
接下来学习一下对窗口模式下的生命周期, 其实多窗口模式并不会改变活动原有的生命周期, 只会将用户最近交互过的那个活动设置为运行状态, 而将多窗口模式下的另外一个可见的活动设置为暂停状态, 如果这时用户又去和暂停的活动进行交互, 那么该活动就变成运行状态, 之前处于运行状态的活动变成暂停状态.
下面我们通过一个例子来更加直观的理解多窗口模式下活动的生命周期:
第一步: 打开 MaterialTest 项目, 修改 MainActivity 中的代码:
这里我们在 Activity 的 7 个生命周期回调方法中分别打印了一句日志.
第二步: 打开 LBSTest 项目, 修改 MainActivity 中的代码:
这里同样也是在 Activity 的 7 个生命周期回调方法中分别打印了一句日志, 注意这两处的日志 TAG 是不一样的, 方便区分.
第三步: 先将 MaterialTest 和 LBSTest 这两个项目中的最新代码都运行到模拟器上, 然后启动 MaterialTest 程序, 这时观察 Logcat 中的打印日志 (注意要将 logcat 的过滤器选择为 No Filters), 如下所示:
可以看到, onCreate(),onStart() 和 onResume() 方法会以此执行, 这个也是在意料之中的事. 然后长按 Overview 按钮, 进入多窗口模式, 此时打印的信息如图所示:
这时发现 MaterialTest 中的 MainActivity 经历了一个重新创建的过程, 其实这个是正常现象, 因为进入多窗口模式后活动的大小发生了比较大的变化, 此时默认是会重新创建活动的, 除此之外, 像横竖屏切换也会重新创建活动的, 进入多窗口模式后, MaterialTest 变成了暂停状态. 接着在 Overview 列表界面选中 LBSTest 程序, 打印信息如图:
可以看到, 现在 LBSTest 的 onCreate(),onStart() 和 onResume() 方法依次得到了执行, 说明现在 LBSTest 变成了运行状态. 接下我们可以随意操作一下 MaterialTest 程序, 然后观察 logcat 中的打印日志, 如图所示:
现在 LBSTest 的 onPause() 方法得到了执行, 而 MaterialTest 的 onResume() 方法得到了执行, 说明 LBSTest 变成了暂停状态, MaterialTest 则变成了运行状态, 这和本小节开头分析的生命周期行为是一致的.
了解了多窗口模式下活动的生命周期规则, 那么我们在编写程序的时候, 就可以将一些关键性的点考虑进去了. 比如说, 在多窗口模式下, 用户仍然可以看到处于暂停状态的应用, 那么像视频播放器之类的应用在此时就应该能继续播放视频才对, 因此, 我们最好不要在活动的 onPause() 方法中去处理视频播放器的暂停逻辑, 而是应该在 onStop() 方法中去处理, 并且在 onStart() 方法恢复视频的播放.
另外, 针对于进入多窗口模式时活动会被重新创建, 如果你想改变这一默认行为, 可以在 AndroidManifest 中对活动进行如下配置:
加入了这行配置之后, 不管是进入多窗口模式, 还是横竖屏切换, 活动都不会被重新创建, 而是会将屏幕发生变化的事件通知到 Activity 的 onConfigurationChanged() 方法当中, 因此, 如果你想在屏幕发生变化的时候进行一些相应的逻辑处理, 那么在活动中重写 onConfigurationChanged() 方法即可.
6.3, 禁用多窗口模式
多窗口模式虽然功能非常强大, 但是未必就适用于所有的程序, 比如说, 手机游戏就非常不适合在多窗口模式下运行, 很难想象我们如何一边玩着游戏, 一边又操作着其他应用, 因此, Android 还是给我们禁用多窗口模式的选项, 如果你非常不希望自己的应用能够在多窗口模式下运行, 那么就可以将这个功能关闭掉.
禁用多窗口模式的方法非常简单, 只需要在 AndroidManifest 的 < application > 或 < activity > 标签中加入如下属性即可:
其中, true 表示应用支持多窗口模式, false 表示应用不支持多窗口模式, 如果不配置这个属性, 那么默认值为 true.
现在将 MaterialTest 程序设置为不支持多窗口模式, 如下所示:
重新运行程序, 然后长按 Overview 按钮, 结果如下所示:
可以看到, 现在是无法进入到多窗口模式的, 而且屏幕下方还会弹出一个 Toast 提示来告知用户, 当前应用不支持多窗口模式.
虽说 Android:resizeableActivity 这个属性的用法很简单, 但是它还存在一个问题, 就是这个属性只有当前项目的 targetSdkVersion 指定成 24 或者更高的时候才会有用, 否则这个属性是无效的, 那么比如说我们将项目的 targetSdkVersion 指定为 23, 这个时候尝试进入多窗口模式, 界面上还是会弹出一个提示, 告知我们此应用在多窗口模式下可能无法正常工作, 但是还是进入了多窗口模式, 这样就非常头疼了, 因为很多老项目, 它们的 targetSdkVersion 都没有指定到 24, 岂不是这些老项目都无法禁用多窗口模式了?
针对这种情况, 还有一种解决方案, Android 规定, 如果项目指定的 targetSdkVersion 低于 24, 并且活动是不允许横竖屏切换的, 那么该应用也将不支持多窗口模式. 默认情况下, 我们的应用都是可以随着手机的旋转自由的横竖屏切换的, 如果想要让应用不允许横竖屏切换, 那么就需要在 AndroidManifest 的 < activity > 标签中加入如下配置:
其中, portrait 表示活动只支持竖屏, landscape 表示活动只支持横屏, 当然 Android:screenOrientation 属性中还有很多其他可选值, 不过最常用的就是 portrait 和 landscape 了.
现在将 MaterialTest 的 MainActivity 设置为只支持竖屏, 如下所示:
重新运行程序之后, 你会发现 MaterialTest 现在不支持横竖屏切换了, 此时长按 Overview 按钮也会弹出不支持多窗口模式的提示, 说明我们已经成功禁用多窗口模式了.
七, Lambda 表达式
Java8 中引入了一些非常有特色的功能, 如 Lambda 表达式, stream API, 接口默认实现等等. 虽然这么多新特性, 但是现在能够立即应用到项目当中的也就只有 Lambda 表达式而已, 因为 stream API 和接口默认实现等特性都只支持 Android7.0 及以上的系统, 显然不能为了使用这些新特性而放弃兼容众多低版本的 Android 手机. 而 Lambda 表达式却最低兼容到 Android2.3 系统, 基本上可以算是覆盖了所有的 Android 手机了.
Lambda 表达式本质是一个匿名方法, 它既没有方法名, 也没有访问修饰符和返回值类型, 使用它来编写代码将会更加简洁, 也更加易读.
如果想要在 Android 项目中使用 Lambda 表达式或者 Java8 的其他新特性, 首先我们需要在 App/build.gradle 中添加如下配置:
之后就可以开始使用 Lambda 表达式来编写代码了, 比如说传统情况下, 开启一个子线程的写法如下:
而使用 Lambda 表达式则可以这样写:
是不是很神奇? 不管从代码行数上还是缩进结构上来看, Lambda 表达式的写法明显要更加精简. 那么为什么我们可以使用这么神奇的写法呢? 这是因为 Thread 类的构造函数接收的参数是一个 Runnable 接口, 并且该接口中只有一个待实现方法, 我们查看一下 Runnable 接口的源码, 如下所示:
凡是这种只有一个待实现方法的接口, 都可以使用 Lambda 表达式的写法, 比如说, 通常创建一个类似于上述接口的匿名类实现需要这样写:
而有了 Lambda 表达式之后我们就可以这样写:
了解了 Lambda 表达式的基本写法, 接下来我们尝试自定义一个接口, 然后再使用 Lambda 表达式的方式进行实现. 新建一个 MyListener 接口, 代码如下所示:
MyListener 接口也只有一个待实现方法, 这和 Runnable 接口的结构是基本一致的, 唯一的不同是, MyListener 中的 doSomething() 方法是有参数并且有返回值的, 那么我们就来看一下这种情况下该如何使用 Lambda 表达式进行实现.
其实写法也是比较相似的, 使用 Lambda 表达式创建 MyListener 接口的匿名实现写法如下:
可以看到, doSomething() 方法的参数直接写在括号里面就可以了, 而返回值则仍然像往常一样, 写在具体实现的最后一行即可.
另外, Java 还可以根据上下文自动推断出 Lambda 表达式中的参数类型, 因此上面的代码也可以简化成如下写法:
Java 将会自动推断出参数 a 是 String 类型, 参数 b 是 int 类型, 从而使得我们的代码变得更加简洁了.
接下来举个具体的例子, 比如说现在有一个方法是接收 MyListener 参数的, 如下所示:
我们在调用 hello() 这个方法的时候就可以这样写:
那么 doSomething() 方法就会将 a 和 b 两个参数进行相加, 从而最终的打印结果就会是 "Hello Lambda1024".
现在已经将 Lambda 表达式的写法基本掌握了, 接下来看一看在 Android 当中有哪些常用的功能是可以使用 Lambda 表达式进行替换的. 其实只要符合接口中只有一个待实现方法这个规则的功能, 都是可以使用 Lambda 表达式编写的. 除了刚才举例说明的开启子线程之外, 还有像设置点击事件之类的功能也是非常适合使用 Lambda 表达式的.
传统情况下, 给一个按钮设置点击事件需要这样写:
而使用 Lambda 表达式之后, 就可以将代码简化成下面这个样子:
另外, 当接口的待实现方法只有一个参数的时候, 我们可以进一步简化, 将参数外面的括号去掉, 如下所示:
这样我们就将 Lambda 表达式的主要内容都掌握了, 当然, 有些人可能并不喜欢 Lambda 表达式这种极简主义的写法, 不管喜欢与否, Java8 对于哪一种写法都是完全支持的, 至于到底要不要使用 Lambda 表达式其实完全凭个人, 多一种选择总归不是一种坏事.
来源: http://www.bubuko.com/infodetail-2935174.html