Android学习-继续进阶

全局获取Context的技巧

很多地方都需要用到Context:
弹出Toast、启动活动、发送广播、操作数据库、使用通知等等。

目前为止没有对得不到Context发愁过,因为很多操作都是在活动中进行的,而活动本身就是一个Context对象。
但当程序的架构逐渐开始复杂起来的时候,很多的逻辑代码都将脱离Activity类,但此时你又恰恰需要使用Context。

举个例子,之前使用HttpUtil类将一些通用的网络操作封装起来,代码如下:
在这里插入图片描述
在这里插入图片描述
这里使用sendHttpRequest方法来发送HTTP请求没有问题,并且还可以在回调方法中处理服务器返回的数据。

但现在相对sendHttpRequest方法进行一些优化,当检测到网络不存在的时候就给用户一个Toast提示,并且不再执行后面的代码。
但是弹出一个Toast需要一个Context参数,在HttpUitl类中显然是获取不到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.xml文件的标签下进行指定:
在这里插入图片描述
注意在这里指定 MyApplication的时候一定要加上完整的包名,不然系统将无法找到这个类。
这样就实现了一种全局获取Context的机制,之后不管想在任何地方使用Context,只需要调用一下MyApplication.getContext就可以了。

接下来再对sendHttpRequest方法进行优化,代码如下:
在这里插入图片描述
可以看到 sendHttpRequest方法不再需要通过传参的方式来得到Context对象,二十调用一下MyApplication .getContext方法就可以了。

回顾当时为了让LitePal可以正常工作,要求必须在AndroidManifest.xml中配置如下内容:
在这里插入图片描述
其实道理是一样的,因为经过这样的配置之后,LitePal就能在内部自动获取到COntext了。

那么如果我们已经配置过了自己的Application怎么办,这样岂不是和LitePalApplication冲突了?

任何一个项目都只能配置一个Application,对于这种情况,LitePal提供了很简单的解决方案,就是在自己的Application中去调用LitePai的初始化方法就可以了,如下:

在这里插入图片描述
这种写法,就相当于把全局的Context对象通过参数传递给了LitePal,效果和再AndroidManifest.xml中配置LItePalApplication是一摸一样的。

使用Intent传递对象
使用Intent可以启动活动、发送广播、启动服务等。

在进行上述操作时,还可以添加一些附加数据,以达到传值的效果,比如在FirstActivity中添加如下代码:

在这里插入图片描述

在Sec中获取:
在这里插入图片描述
但putExtra方法中所支持的数据类型是有限的,虽然常用的一些数据类型它都会支持,但是当你想传递一些自定义对象的时候,就无从下手。

接下来学习使用Intent传递对象的技巧。

Serializable方式

使用Intent来传递对象通常由两种方式:Serializable和Parcelable。

Serializable是序列化的意思,表示将一个 对象转换成可存储或可传输的状态。序列化后的对象可以在网络上进行传输,也可以存储到本地。至于序列化的方法也很简单,只需要让一个类去实现Serializable这个接口就可以了。

比如说有一个Person类,其中包含了name和age这两个字段,想要将它序列化就可以这样写:
在这里插入图片描述
Person实现了Serializable接口,这样所有的Person对象就都是可序列化的了。

接下来在FirstAcitivity中:
在这里插入图片描述
这里创建了一个Person的实例,然后就直接将他传入到putExtra方法中了。由于Person类实现了Serializable接口,所以才可以这样写。

接下来在sec中获取这个对象:
在这里插入图片描述
这里调用了getSerializableExtra方法来获取通过参数传递过来的序列化对象,接着再向下转型成Person对象,这样就成功实现了使用Intent来传递对象的功能了。

Parcelable方式
(包裹化)

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

下面看一下Parcelable的实现方式,修改Person中代码。如图:
在这里插入图片描述
首先,让Person类去实现了Parcelabele接口,这样就必须重写describeContents和writeToParcel这两个方法。其中describeContents方法直接返回0就可以了,而writeToParcel方法中我们需要调用Parcel的writeXXX方法,将Person类中的字段一一写出。
注意,字符串类型数据就调用writeString方法,整形数据就调用WriteInt方法,以此类推。

除此之外,还必须在Person类中提供一个名为CREATOR的常量,这里创建了Parcelable.Creator接口的一个实现,并将泛型指定为Person。
接着需要重写createFromParcel和newArray这两个方法,在createFromParcel方法中需要读取刚才写出的name和age字段,并创建一个Person对象进行返回,其中name和age都是调用Parcel的readXXX方法读取到的。
注意这里的读取顺序一定要和刚才写出的顺序完全相同。
而newArray方法就是new出一个Person数组,并使用方法中传入size作为数组大小就可以了。
接下来在Fir中我们仍可以使用相同的代码来传递Person对象,只不过在Sec中获取对象的时候需要稍加改动,如下:
在这里插入图片描述
(这里的参数是自定义的吗??)
注意,这里不再是调用getSerializableExtra方法,而是调用getParcelableExtra方法来获取传递过来的对象了,其他地方都相同。
这样就把使用Intent传递对象的两种实现方式都学完了,对比一下Serializable的方式较为简单,但是由于会把整个对象进行序列化,因此效率会比Parcelable方式低一些,所以在通常情况下更推荐使用Parcelable方式来实现Intent传递对象的功能。

定制自己的日志工具

Android自带的日志工具很强大,但有一些缺点,例如在打印日志的控制方面就做的不够好。

打个比方,正在编写一个比较庞大的项目,期间为了方便调试,在代码的很多地方都打印了大量的日志,最近项目已经基本完成但是却有一个问题,之前调试的日志,在项目正式向先之后仍然会照常打印,这样不仅会降低程序的运行效率,还有可能将一些机密性的数据泄露出去。

肯定不能一行一行把所有打印日志的代码全都删掉。

最理想的情况就是自由的控制日志的打印,当程序处于开发阶段的时候就让日志打印出来,当程序上线了之后就把日志屏蔽掉。

自定义日志工具:

新建一个LogUtil类,代码如下:
在这里插入图片描述
在这里插入图片描述
首先定义了六个整型常量,并且它们对应的值都是递增的。然后又定义了一个静态变量level,可以将它的值指定为上面六个常量中的任意一个。

接下来提供了五个自定义的方法日志,在其内部分别调用了Log方法来打印日志,只不过在这些自定义的方法中我们都加入了一个if判断,只有当level的小于或等于对应日志级别的时候才会将日志打印出来。

这样就定义好了个自定义的日志工具,比如打印一行DEBUG级别和WARN级别的日志:

在这里插入图片描述
在这里插入图片描述
使用这种方法后,只需要在开发阶段将level指定为BERBOSE,在项目上线的时候指定成NOTHING就可以了。

调试Android程序

当开发过程中遇到一些奇怪的bug,但又迟迟定位不出来的原因是是什么时候,最好的解决办法就是调试了。
(急需!!!)

调试允许我们逐行执行代码,并可以实时观察内存中的数据,从而能够比较轻易的查出问题。

放断点,然后dubug,F8向下执行。Variables视图中可以看到内存中的数据。
(调试的时候调着调着跑底层去了=。=)

这种调试方法虽然完全可以正常工作,但在调试模式下,程序的运行效率会大大降低,如果你的断点加载一个比较靠后的位置,需要执行很多的操作才能运行到这个断点,那么前面这些操作都会有一个卡顿的感觉。

Android还提供了另一种调试的方式,可以让程序随时进入到调试模式。

这次不需要选择调试模式来启动程序了,就是用正常的方式来启动程序。由于现在不是在调试模式下,程序的运行速度比较快,可以先将账号和密码输入好。然后点击run下的Attach debugger to Android process按钮。

此时会弹出一个线程选择提示框。

选择后进入调试模式,接下来流程就相同了。(不会用啊怎么。。怎么不出现调试的框框)

创建定时任务

Android中的定时任务一般有两种实现方式,一种是使用JAVA API里提供的Timer类,一种是使用Android的Alarm机制。这两种方式在多数情况下都能实现类似的效果。

但Timer有一个明显的短板,它并不太适用于那些需要长期在后台运行的定时任务。

为了让电池更加耐用,每种手机都有自己的休眠策略,Android手机就会在长时间不操作的情况下自动让CPU进入到睡眠状态,这就可能导致Timer中的定时任务无法正常运行。而Alarm则具有唤醒CPU的功能,它可以保证在大多数情况下需要执行定时任务的时候CPU都能正常工作。需要注意,这里唤醒CPU和唤醒屏幕完全不是一个概念,千万不要产生混淆。

Alarm机制

主要是借助了AlarmManager类来实现的。
这个类和NotificationManager有点类似,都是通过调用Context的getSystemService方法来获取实例的,只是这里需要传入的参数是Context.ALARM_SERVICE。因此,获取一个AlarmManager的实例就可以写成:
在这里插入图片描述
接下来调用AlarmManager的set方法就可以设定一个定时任务了,比如说想要设定一个任务在十秒钟后执行,就可以写成:
在这里插入图片描述
set方法中有三个参数。
第一个参数是整型参数,用于指定AlarmManager的工作类型,有4种值可以选,分别是ELAPSED_REALTIME、ELAPSED_REALTIME_WAKEUP、RTC和RTC_WAKEUP。
其中ELAPSED_REALTIME表示让定时任务的出发时间从系统开机开始算起,但不会唤醒CPU。
ELAPSED_REALTIME_WAKEUP同样表示让定时任务的出发时间从系统开机开始算起,但会唤醒CPU。
RTC表示让定时任务的出发时间从1970年1月1日0点开始算起,但不会唤醒CPU。
RTC_WAKEUP表示让定时任务的出发时间从1970年1月1日0点开始算起,会唤醒CPU。

使用SystemClock.elapsedRealtime方法可以获取到系统开机至今所经历时间的毫秒数,使用System.currentTimeMilllis方法可以获取到1970年1月1日0点至今所经历时间的毫秒数。

然后看下第二个参数,就是定时任务触发的时间,以毫秒为单位,如果第一个参数使用的是ELAPSED_REALTIME或者ELAPSED_REALTIME_WAKEUP,则这里传入开机至今的时间再加上延迟执行的时间。

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

那么设置一个任务在十秒钟后执行也可以写成:
在这里插入图片描述
那么如果要实现一个长时间在后台定时运行的服务该怎么做呢?
首先新建一个很普通的服务,比如起名叫LongRunningService,然后将触发定时任务的代码写道onStartCommand方法中,如下:
在这里插入图片描述
可以看到,首先在onStartCommand中开启了一个子线程,这样就可以执行具体的逻辑操作了。之所以要在子线程中执行逻辑操作,是因为逻辑操作也是需要耗时的,如果放在主线程里执行可能会对定时任务的准确性造成轻微的影响。

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

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

最后只需要在你想要启动定时服务的时候调用如下代码:
在这里插入图片描述

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

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

Doze模式

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

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

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

图完整的描述
在这里插入图片描述

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

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

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

多窗口模式编程

由于手机屏幕大小的限制,传统情况下一个手机只能同时打开一个应用程序,无论是Android、ios还是windows phone都是如此。我们也早就对此习以为常,认为这是理所当然的事情。而Android7.0系统中引入了一个多窗口模式,它允许我们在同一个屏幕中同时打开两个应用程序。

进入多窗口模式

首先不用编写任何额外的代码来让应用程序支持多窗口模式。
事实上,书中所有的项目都是支持多窗口模式的。但这并不意味着我们就不需要对多窗口模式进行学习,因为系统化的了解这些知识点才能编写出在多窗口模式下兼容性更好的程序。

首先看下如何才能进入多窗口模式。

手机的导航栏有三个按钮
在这里插入图片描述
左边的back和中间的home经常使用,但是右边的Overview按钮使用就比较少。
这个按钮的作用是打开一个最近访问过的活动或者任务的列表,从而能够方便的在多个应用程序之间进行切换。

可以通过两种方式进入多窗口模式:
1.在Overview列表界面长按任意一个活动的标题,将该活动拖动到屏幕突出显示的区域则可以进入多窗口模式
2.打开一个程序,长按Overview按钮,也可以进入多窗口模式。

(现在没有导航栏了吧。。)

多窗口模式下,整个应用的界面会缩小很多,那么编写程序时就应该多多考虑使用match_parent属性、RecyclerView、ListView、ScrllView等控件,来让应用的界面更能够适配不同尺寸的屏幕。

多窗口模式下的生命周期

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

例子:
首先打开MaterialTest项目,修改MainActivity中的代码:
在这里插入图片描述
在这里插入图片描述
这里在Activity的七个生命周期回调方法中分别打印了一句日志。

然后打开LBSTest项目,修改MainActivity代码:
在这里插入图片描述
在这里插入图片描述
这里也在生命周期回调方法中分别打印了一句日志。
注意这两处日志的TAG时不一样的,方便我们进行区分。

现在将MaterialTest和LBSTest这两个项目都运行到模拟器上,然后启动MaterTest项目。观察logcat中的打印日志(注意将logcat的过滤器选择为No Filters)
如图:
在这里插入图片描述

然后长按Overview按钮,进入多窗口模式,如图:
在这里插入图片描述
会发现此时的MaterialTest中的MainActivity经历了一个重新创建的过程。
其实这是个正常现象,因为进入多窗口模式后活动的大小发生了比较大的变化,此时默认时会重新创建活动的。
除此之外像横竖屏切换也是会重新创建活动的。进入多窗口模式后,MaterTest变成了暂停状态。

接着在Overview列表界面中选中LBSTest程序,如图:
在这里插入图片描述
可以看到,现在LBSTest变成了运行状态。

接下来可以随意操作一下MaterialTest,观察logcat,如图:
在这里插入图片描述

现在LBSTest变成了暂停状态,而MaterialTest变成了运行状态。

了解了多窗口下生命周期的规则后,在编写程序的时候就可以将一些关键性的点考虑进去了。
比如说,在多窗口模式下,用户仍然可以看到处于暂停状态的应用,那么像视频播放器之类的应用在此时就应该能继续播放视频才对。
因此最好不要再onPause方法中去处理视频播放器的暂停逻辑,而是应该在onStop中处理,并在onStart方法中回复视频的播放。

另外,针对于进入多窗口模式时活动会被重新创建,如果想改变这一默认行为,可以在AndroidManifest.xml中对活动进行如下配置:
在这里插入图片描述
加了这行配置后,不管是进入多窗口模式还是横竖屏切换,活动都不会被重新创建,而是会将屏幕变化的事件通知到Activity的onConfighurationChanged方法中。因此,如果你想在屏幕发生变化的时候进行相应的逻辑处理,那么在活动中重写onConfigurationChanged方法即可。

禁用多窗口模式

多窗口功能强大,但是未必适用于所有的程序。

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

禁用多窗口模式的方法只需要在AndroidManifest.xml的或标签中加入如下属性即可:
在这里插入图片描述

其中true表示支持,fasle不支持,如果不配置这个属性默认为true。

现在将MaterialTest改为不支持:
在这里插入图片描述

虽说android:resizeableActivity这个属性的用法挺简单的,但是还存在一个问题,就是这个属性只有当项目的targetSdkVersion指定成24或者更高的时候才会用,否则这个属性是无效的。

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

默认情况下,应用都可以随着手机的旋转自由的横竖屏切换,如果想要应用不允许横竖屏切换,那么就需要在AndroidManifest.xml的中加入如下配置:
在这里插入图片描述
其中prtrait表示活动支支持竖屏,landscape表示活动只支持横屏。
当然android:screenOrientation属性中还有很多其他可选值,不过最常用的就是这两个了。

现在设置成只支持竖屏:
在这里插入图片描述

Lanmbda表达式

本质是一种匿名方法,没有方法名也没有访问修饰符和返沪值类型,用它编写代码将会更简洁也更加易读。

如果想在Android项目中使用它,或者java8的新特性,需要在app/build.gradle中添加如下配置:
在这里插入图片描述
之后就可以使用啦。
比如传统情况下开启一个子线程的写法如下:
在这里插入图片描述
可以这项写的原因:
Thread类的构造函数接收的参数是一个Runnable接口,并且该接口中只有一个待实现的方法。
Runnable接口的源码:
在这里插入图片描述
凡是这种只有一个待实现方法的接口,都可以使用拉姆达表达式。

比如通常创建一个类似于上述接口的匿名实现需要这样写:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值