1 Activity的生命周期和启动模式
1.1 Activity的生命周期全面分析
用户正常使用情况下的生命周期 & 由于Activity被系统回收或者设备配置改变导致Activity被销毁重建情况下的生命周期。
1.1.1 典型情况下的生命周期分析
Activity的生命周期和启动模式
Activity第一次启动:onCreate->onStart->onResume。
Activity切换到后台( 用户打开新的Activity或者切换到桌面) ,onPause->onStop(如果新Activity采用了透明主题,则当前Activity不会回调onstop)。
Activity从后台到前台,重新可见,onRestart->onStart->onResume。
用户退出Activity,onPause->onStop->onDestroy。
onStart开始到onStop之前,Activity可见。onResume到onPause之前,Activity可以接受用户交互。
在新Activity启动之前,栈顶的Activity需要先onPause后,新Activity才能启动。所以不能在onPause执行耗时操作。
onstop中也不可以太耗时,资源回收和释放可以放在onDestroy中。
1.1.2 异常情况下的生命周期分析
1 系统配置变化导致Activity销毁重建
例如Activity处于竖屏状态,如果突然旋转屏幕,由于系统配置发生了改变,Activity就会被销
毁并重新创建。
在异常情况下系统会在onStop之前调用onSaveInstanceState来保存状态。Activity重新创建后,会在onStart之后调用onRestoreInstanceState来恢复之前保存的数据。
保存数据的流程: Activity被意外终止,调用onSaveIntanceState保存数据-> Activity委托Window,Window委托它上面的顶级容器一个ViewGroup( 可能是DecorView) 。然后顶层容器在通知所有子元素来保存数据。
这是一种委托思想,Android中类似的还有:View绘制过程、事件分发等。
系统只在Activity异常终止的时候才会调用 onSaveInstanceState 和onRestoreInstanceState 方法。其他情况不会触发。
2 资源内存不足导致低优先级的Activity被回收
三种Activity优先级:前台- 可见非前台 -后台,从高到低。
如果一个进程没有四大组件,那么将很快被系统杀死。因此,后台工作最好放入service中。
android:configChanges="orientation" 在manifest中指定 configChanges 在系统配置变化后不重新创建Activity,也不会执行 onSaveInstanceState 和onRestoreInstanceState 方法,而是调用 onConfigurationChnaged 方法。
configChanges 一般常用三个选项:
locale 系统语言变化
keyborardHidden 键盘的可访问性发生了变化,比如用户调出了键盘
orientation 屏幕方向变化
1.2 Activity的启动模式
1.2.1 Activity的LaunchMode
Android使用栈来管理Activity。
standard
每次启动都会重新创建一个实例,不管这个Activity在栈中是否已经存在。谁启动了这个Activity,那么Activity就运行在启动它的那个Activity所在的栈中。
用Application去启动Activity时会报错,原因是非Activity的Context没有任务栈。解决办法是为待启动Activity制定FLAG_ACTIVITY_NEW_TASH标志位,这样就会为它创建一个新的任务栈。
singleTop
如果新Activity位于任务栈的栈顶,那么此Activity不会被重新创建,同时回调 onNewIntent 方法。onCreate和onStart方法不会被执行。
singleTask
这是一种单实例模式。如果不存在activity所需要的任务栈,则创建一个新任务栈和新Activity实例;如果存在所需要的任务栈,不存在实例,则新创建一个Activity实例;如果存在所需要的任务栈和实例,则不创建,调用onNewIntent方法。同时使该Activity实例之上的所有Activity出栈。
参考:taskAffinity标识Activity所需要的任务栈
singleIntance
单实例模式。具有singleTask模式的所有特性,同时具有此模式的Activity只能独自位于一个任务栈中。
假设两个任务栈,前台任务栈为12,后台任务栈为XY。Y的启动模式是singleTask。现在请求Y,整个后台任务栈会被切换到前台。如图所示:
设置启动模式
manifest中 设置下的 android:launchMode 属性。
启动Activity的 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 。
两种同时存在时,以第二种为准。第一种方式无法直接为Activity添加FLAG_ACTIVITY_CLEAR_TOP标识,第二种方式无法指定singleInstance模式。
可以通过命令行 adb shell dumpsys activity 命令查看栈中的Activity信息。
1.2.2 Activity的Flags
这些FLAG可以设定启动模式、可以影响Activity的运行状态。
FLAG_ACTIVITY_NEW_TASK
为Activity指定“singleTask”启动模式。
FLAG_ACTIVITY_SINGLE_TOP
为Activity指定“singleTop"启动模式。
FLAG_ACTIVITY_CLEAR_TOP
具有此标记位的Activity启动时,同一个任务栈中位于它上面的Activity都要出栈,一般和FLAG_ACTIVITY_NEW_TASK配合使用。
FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
如果设置,新的Activity不会在最近启动的Activity的列表(就是安卓手机里显示最近打开的Activity那个系统级的UI)中保存。等同于在xml中指定android:exludeFromRecents="true"属性。
1.3 IntentFilter的匹配规则
Activity调用方式
显示调用 明确指定被启动对象的组件信息,包括包名和类名
隐式调用 不需要明确指定组件信息,需要Intent能够匹配目标组件中的IntentFilter中所设置的过滤信息。
匹配规则
IntentFilter中的过滤信息有action、category、data。
只有一个Intent同时匹配action类别、category类别、data类别才能成功启动目标Activity。
一个Activity可以有多个intent-filter,一个Intent只要能匹配任何一组intent-filter即可成功启动对应的Activity。
** action**
action是一个字符串,匹配是指与action的字符串完全一样,区分大小写。
一个intent-filter可以有多个aciton,只要Intent中的action能够和任何一个action相同即可成功匹配。
Intent中如果没有指定action,那么匹配失败。
** category**
category是一个字符串。
Intent可以没有category,但是如果你一旦有category,不管有几个,每个都必须与intent-filter中的其中一个category相同。
系统在 startActivity 和 startActivityForResult 的时候,会默认为Intent加上 android.intent.category.DEFAULT 这个category,所以为了我们的activity能够接收隐式调用,就必须在intent-filter中加上 android.intent.category.DEFAULT 这个category。
** data**
data的匹配规则与action一样,如果intent-filter中定义了data,那么Intent中必须要定义可匹配的data。
intent-filter中data的语法:
<data android:scheme="string" android:host="string" android:port="string" android:path="string" android:pathPattern="string" android:pathPrefix="string" android:mimeType="string"/>
Intent中的data有两部分组成:mimeType和URI。mimeType是指媒体类型,比如
image/jpeg、audio/mpeg4-generic和video/等,可以表示图片、文本、视频等不同的媒
体格式。
URI的结构:
<scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]
实际例子
content://com.example.project:200/folder/subfolder/etc http://www.baidu.com:80/search/info
scheme:URI的模式,比如http、file、content等,默认值是 file 。
host:URI的主机名
port:URI的端口号
path、pathPattern和pathPrefix:这三个参数描述路径信息。
path、pathPattern可以表示完整的路径信息,其中pathPattern可以包含通配符 * ,表示0个或者多个任意字符。
pathPrefix只表示路径的前缀信息。
过滤规则的uri为空时,有默认值content和file,因此intent设置uri的scheme部分必须为content或file。
Intent指定data时,必须调用 setDataAndType 方法, setData 和 setType 会清除另一方的值。
对于service和BroadcastReceiver也是同样的匹配规则,不过对于service最好使用显式调用。
隐式调用需注意
当通过隐式调用启动Activity时,没找到对应的Activity系统就会抛出 android.content.ActivityNotFoundException 异常,所以需要判断是否有Activity能够匹配我们的隐式Intent。
采用 PackageManager 的 resloveActivity 方法或Intent 的 resloveActivity 方法
public abstract List<ResolveInfo> queryIntentActivityies(Intent intent,int flags);
public abstract ResolveInfo resloveActivity(Intent intent,int flags);
以上的第二个参数使用 MATCH_DEFAULT_ONLY ,这个标志位的含义是仅仅匹配那些在
intent-filter中声明了 android.intent.category.DEFAULT 这个category的Activity。因为如果把不含这个category的Activity匹配出来了,由于不含DEFAULT这个category的Activity是无法接受隐式Intent的从而导致startActivity失败。
下面的action和category用来表明这是一个入口Activity,并且会出现在系统的应用列表中,二者缺一不可。
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
2 IPC机制
2.1 Android IPC 简介
IPC即Inter-Process Communication,含义为进程间通信或者跨进程通信,是指两个进程之间进行数据交换的过程。
线程是CPU调度的最小单元,是一种有限的系统资源。进程一般指一个执行单元,在PC和移动设备上是指一个程序或者应用。进程与线程是包含与被包含的关系。一个进程可以包含多个线程。最简单的情况下一个进程只有一个线程,即主线程( 例如Android的UI线程) 。
任何操作系统都需要有相应的IPC机制。如Windows上的剪贴板、管道和邮槽;Linux上命名管道、共享内容、信号量等。Android中最有特色的进程间通信方式就是binder,另外还支持socket。contentProvider是Android底层实现的进程间通信。
在Android中,IPC的使用场景大概有以下:
有些模块由于特殊原因需要运行在单独的进程中。
通过多进程来获取多份内存空间。
当前应用需要向其他应用获取数据。
2.2 Android中的多进程模式
2.2.1 开启多进程模式
在Android中使用多线程只有一种方法:给四大组件在Manifest中指定 android:process 属性。这个属性的值就是进程名。这意味着不能在运行时指定一个线程所在的进程。
tips:使用 adb shell ps 或 adb shell ps|grep 包名 查看当前所存在的进程信息。
两种进程命名方式的区别
“:remote”
“:”的含义是指在当前的进程名前面附加上当前的包名,完整的进程名为“com.example.c2.remote"。这种进程属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中。
"com.example.c2.remote"
这是一种完整的命名方式。这种进程属于全局进程,其他应用可以通过ShareUID方式和它跑在同一个进程中。
2.2.2 多线程模式的运行机制
Android为每个进程都分配了一个独立的虚拟机,不同虚拟机在内存分配上有不同的地址空间,导致不同的虚拟机访问同一个类的对象会产生多份副本。例如不同进程的Activity对静态变量的修改,对其他进程不会造成任何影响。所有运行在不同进程的四大组件,只要它们之间需要通过内存在共享数据,都会共享失败。四大组件之间不可能不通过中间层来共享数据。
多进程会带来以下问题:
静态成员和单例模式完全失效。
线程同步锁机制完全失效。
这两点都是因为不同进程不在同一个内存空间下,锁的对象也不是同一个对象。
SharedPreferences的可靠性下降。
SharedPreferences底层是 通过读/写XML文件实现的,并发读/写会导致一定几率的数据丢失。
Application会多次创建。
由于系统创建新的进程的同时分配独立虚拟机,其实这就是启动一个应用的过程。在多进程模式中,不同进程的组件拥有独立的虚拟机、Application以及内存空间。
多进程相当于两个不同的应用采用了SharedUID的模式
实现跨进程的方式有很多:
Intent传递数据。
共享文件和SharedPreferences。
基于Binder的Messenger和AIDL。
Socket等
2.3 IPC基础概念介绍
主要介绍 Serializable 、 Parcelable 、 Binder 。Serializable和Parcelable接口可以完成对象的序列化过程,我们通过Intent和Binder传输数据时就需要Parcelabel和Serializable。还有的时候我们需要对象持久化到存储设备上或者通过网络传输到其他客户端,也需要Serializable完成对象持久化。
2.3.1 Serializable接口
Serializable 是Java提供的一个序列化接口( 空接口) ,为对象提供标准的序列化和反序列化操作。只需要一个类去实现 Serializable 接口并声明一个 serialVersionUID 即可实现序列化。
private static final long serialVersionUID = 8711368828010083044L
serialVersionUID也可以不声明。如果不手动指定 serialVersionUID 的值,反序列化时如果当前类有所改变( 比如增删了某些成员变量) ,那么系统就会重新计算当前类的hash值并更新 serialVersionUID 。这个时候当前类的 serialVersionUID 就和序列化数据中的serialVersionUID 不一致,导致反序列化失败,程序就出现crash。
静态成员变量属于类不属于对象,不参与序列化过程,其次 transient 关键字标记的成员变量也不参与序列化过程。
通过重写writeObject和readObject方法可以改变系统默认的序列化过程。
2.3.2 Parcelable接口
Parcel内部包装了可序列化的数据,可以在Binder中自由传输。序列化过程中需要实现的功能有序列化、反序列化和内容描述。
序列化功能由 writeToParcel 方法完成,最终是通过 Parcel 的一系列writer方法来完成。
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeInt(code);
out.writeString(name);
}
反序列化功能由 CREATOR 来完成,其内部表明了如何创建序列化对象和数组,通过 Parcel 的一系列read方法来完成。
public static final Creator<Book> CREATOR = new Creator<Book>() {@Overridepublic Book createFromParcel(Parcel in) {return new Book(in);
} @Overridepublic Book[] newArray(int size) {return new Book[size];
}
};protected Book(Parcel in) {
code = in.readInt();
name = in.readString();
}
在Book(Parcel in)方法中,如果有一个成员变量是另一个可序列化对象,在反序列化过程中需要传递当前线程的上下文类加载器,否则会报无法找到类的错误。
book = in.readParcelable(Thread.currentThread().getContextClassLoader());
内容描述功能由 describeContents 方法完成,几乎所有情况下都应该返回0,仅当当前对象中存在文件描述符时返回1。
public int describeContents() {return 0;
}
Serializable 是Java的序列化接口,使用简单但开销大,序列化和反序列化过程需要大量I/O操作。而 Parcelable 是Android中的序列化方式,适合在Android平台使用,效率高但是使用麻烦。 Parcelable 主要在内存序列化上,Parcelable 也可以将对象序列化到存储设备中或者将对象序列化后通过网络传输,但是稍显复杂,推荐使用 Serializable 。
2.3.3 Binder
Binder是Android中的一个类,实现了 IBinder 接口。从IPC角度说,
Binder是Andoird的一种跨进程通讯方式,Binder还可以理解为一种虚拟物理设备,它的设备驱动是/dev/binder。从Android Framework角度来说,Binder是 ServiceManager 连接各种Manager( ActivityManager· 、 WindowManager ) 和相应 ManagerService 的桥梁。从Android应用层来说,Binder是客户端和服务端进行通信的媒介,当bindService时,服务端返回一个包含服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务器端提供的服务或者数据( 包括普通服务和基于AIDL的服务)。
Binder通信采用C/S架构,从组件视角来说,包含Client、Server、ServiceManager以及binder驱动,其中ServiceManager用于管理系统中的各种服务。
图中的Client,Server,Service Manager之间交互都是虚线表示,是由于它们彼此之间不是直接交互的,而是都通过与Binder驱动进行交互的,从而实现IPC通信方式。其中Binder驱动位于内核空间,Client,Server,Service Manager位于用户空间。Binder驱动和Service Manager可以看做是Android平台的基础架构,而Client和Server是Android的应用层,开发人员只需自定义实现client、Server端,借助Android的基本平台架构便可以直接进行IPC通信。
http://gityuan.com/2015/10/31/binder-prepare/
Android中Binder主要用于 Service ,包括AIDL和Messenger。普通Service的Binder不涉及进程间通信,Messenger的底层其实是AIDL,所以下面通过AIDL分析Binder的工作机制。
由系统根据AIDL文件自动生成.java文件
Book.java
表示图书信息的实体类,实现了Parcelable接口。
Book.aidl
Book类在AIDL中的声明。
IBookManager.aidl
定义的管理Book实体的一个接口,包含 getBookList 和 addBook 两个方法。尽管Book类和IBookManager位于相同的包中,但是在IBookManager仍然要导入Book类。
IBookManager.java
系统为IBookManager.aidl生产的Binder类,在 gen 目录下。
IBookManager继承了 IInterface 接口,所有在Binder中传输的接口都需要继IInterface接口。结构如下:
声明了 getBookList 和 addBook 方法,还声明了两个整型id分别标识这两个方法,用于标识在 transact 过程中客户端请求的到底是哪个方法。
声明了一个内部类 Stub ,这个 Stub 就是一个Binder类,当客户端和服务端位于同一进程时,方法调用不会走跨进程的 transact 。当二者位于不同进程时,方法调用需要走 transact 过程,这个逻辑有 Stub 的内部代理类 Proxy 来完成。
这个接口的核心实现就是它的内部类 Stub 和 Stub 的内部代理类 Proxy 。
Stub和Proxy类的内部方法和定义
DESCRIPTOR
Binder的唯一标识,一般用Binder的类名表示。
asInterface(android.os.IBinder obj)
将服务端的Binder对象转换为客户端所需的AIDL接口类型的对象,如果C/S位于同一进
程,此方法返回就是服务端的Stub对象本身,否则返回的就是系统封装后的Stub.proxy对
象。
asBinder
返回当前Binder对象。
onTransact
这个方法运行在服务端的Binder线程池中,由客户端发起跨进程请求时,远程请求会通过
系统底层封装后交由此方法来处理。该方法的原型是
java public Boolean onTransact(int code,Parcelable data,Parcelable reply,int flags)
服务端通过code确定客户端请求的目标方法是什么,
接着从data取出目标方法所需的参数,然后执行目标方法。
执行完毕后向reply写入返回值( 如果有返回值) 。
如果这个方法返回值为false,那么服务端的请求会失败,利用这个特性我们可以来做权限验证。
Proxy#getBookList 和Proxy#addBook
这两个方法运行在客户端,内部实现过程如下:
首先创建该方法所需要的输入型对象Parcel对象_data,输出型Parcel对象_reply和返回值对象List。
然后把该方法的参数信息写入_data( 如果有参数)
接着调用transact方法发起RPC( 远程过程调用) ,同时当前线程挂起
然后服务端的onTransact方法会被调用知道RPC过程返回后,当前线程继续执行,并从_reply中取出RPC过程的返回结果,最后返回_reply中的数据。
AIDL文件不是必须的,之所以提供AIDL文件,是为了方便系统为我们生成IBookManager.java,但我们完全可以自己写一个。
linkToDeath和unlinkToDeath
如果服务端进程异常终止,我们到服务端的Binder连接断裂。但是,如果我们不知道Binder连接已经断裂,那么客户端功能会受影响。通过linkTODeath我们可以给Binder设置一个死亡代理,当Binder死亡时,我们就会收到通知。
声明一个 DeathRecipient 对象。 DeathRecipient 是一个接口,只有一个方法 binderDied ,当Binder死亡的时候,系统就会回调 binderDied 方法,然后我们就可以重新绑定远程服务。
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient(){
@Override
public void binderDied(){
if(mBookManager == null){
return;
}
mBookManager.asBinder().unlinkToDeath(mDeathRecipient,0);
mBookManager = null;
// TODO:这里重新绑定远程Service
}
}
在客户端绑定远程服务成功后,给binder设置死亡代理:
mService = IBookManager.Stub.asInterface(binder);
binder.linkToDeath(mDeathRecipient,0);
另外,可以通过Binder的 isBinderAlive 判断Binder是否死亡。
2.4 Android中的IPC方式
主要有以下方式:
Intent中附加extras
共享文件
Binder
ContentProvider
Socket
2.4.1 使用Bundle
四大组件中的三大组件( Activity、Service、Receiver) 都支持在Intent中传递 Bundle 数据。
Bundle实现了Parcelable接口,因此可以方便的在不同进程间传输。当我们在一个进程中启动了另一个进程的Activity、Service、Receiver,可以再Bundle中附加我们需要传输给远程进程的消息并通过Intent发送出去。被传输的数据必须能够被序列化。
2.4.2 使用文件共享
我们可以序列化一个对象到文件系统中的同时从另一个进程中恢复这个对象。
通过 ObjectOutputStream / ObjectInputStream 序列化一个对象到文件中,或者在另一个进程从文件中反序列这个对象。注意:反序列化得到的对象只是内容上和序列化之前的对象一样,本质是两个对象。
文件并发读写会导致读出的对象可能不是最新的,并发写的话那就更严重了 。所以文件共享方式适合对数据同步要求不高的进程之间进行通信,并且要妥善处理并发读写问题。
SharedPreferences 底层实现采用XML文件来存储键值对。系统对它的读/写有一定的缓存策略,即在内存中会有一份 SharedPreferences 文件的缓存,因此在多进程模式下,系统对它的读/写变得不可靠,面对高并发读/写时 SharedPreferences 有很大几率丢失数据,因此不建议在IPC中使用 SharedPreferences 。
2.4.3 使用Messenger
Messenger可以在不同进程间传递Message对象。是一种轻量级的IPC方案,底层实现是AIDL。它对AIDL进行了封装,使得我们可以更简便的进行IPC。
具体使用时,分为服务端和客户端:
服务端:创建一个Service来处理客户端请求,同时创建一个Handler并通过它来创建一个
Messenger,然后再Service的onBind中返回Messenger对象底层的Binder即可。
private final Messenger mMessenger = new Messenger (new xxxHandler());
客户端:绑定服务端的Sevice,利用服务端返回的IBinder对象来创建一个Messenger,通过这个Messenger就可以向服务端发送消息了,消息类型是 Message 。如果需要服务端响应,则需要创建一个Handler并通过它来创建一个Messenger( 和服务端一样) ,并通过 Message 的 replyTo 参数传递给服务端。服务端通过Message的 replyTo 参数就可以回应客户端了。
总而言之,就是客户端和服务端 拿到对方的Messenger来发送 Message 。只不过客户端通过bindService 而服务端通过 message.replyTo 来获得对方的Messenger。
Messenger中有一个 Hanlder 以串行的方式处理队列中的消息。不存在并发执行,因此我们不用考虑线程同步的问题。
2.4.4 使用AIDL
如果有大量的并发请求,使用Messenger就不太适合,同时如果需要跨进程调用服务端的方法,Messenger就无法做到了。这时我们可以使用AIDL。
流程如下:
服务端需要创建Service来监听客户端请求,然后创建一个AIDL文件,将暴露给客户端的接口在AIDL文件中声明,最后在Service中实现这个AIDL接口即可。
客户端首先绑定服务端的Service,绑定成功后,将服务端返回的Binder对象转成AIDL接口所属的类型,接着就可以调用AIDL中的方法了。
AIDL支持的数据类型:
基本数据类型、String、CharSequence
List:只支持ArrayList,里面的每个元素必须被AIDL支持
Map:只支持HashMap,里面的每个元素必须被AIDL支持
Parcelable
所有的AIDL接口本身也可以在AIDL文件中使用
自定义的Parcelable对象和AIDL对象,不管它们与当前的AIDL文件是否位于同一个包,都必须显式import进来。
如果AIDL文件中使用了自定义的Parcelable对象,就必须新建一个和它同名的AIDL文件,并在其中声明它为Parcelable类型。
package com.ryg.chapter_2.aidl;
parcelable Book;
AIDL接口中的参数除了基本类型以外都必须表明方向in/out。AIDL接口文件中只支持方法,不支持声明静态常量。建议把所有和AIDL相关的类和文件放在同一个包中,方便管理。
void addBook(in Book book);
AIDL方法是在服务端的Binder线程池中执行的,因此当多个客户端同时连接时,管理数据的集合直接采用 CopyOnWriteArrayList 来进行自动线程同步。类似的还有 ConcurrentHashMap 。
因为客户端的listener和服务端的listener不是同一个对象,所以 RecmoteCallbackList 是系统专门提供用于删除跨进程listener的接口,支持管理任意的AIDL接口,因为所有AIDL接口都继承自 IInterface 接口。
public class RemoteCallbackList<E extends IInterface>
它内部通过一个Map接口来保存所有的AIDL回调,这个Map的key是 IBinder 类型,value是 Callback 类型。当客户端解除注册时,遍历服务端所有listener,找到和客户端listener具有相同Binder对象的服务端listenr并把它删掉。
==客户端RPC的时候线程会被挂起,由于被调用的方法运行在服务端的Binder线程池中,可能很耗时,不能在主线程中去调用服务端的方法。==
权限验证
默认情况下,我们的远程服务任何人都可以连接,我们必须加入权限验证功能,权限验证失败则无法调用服务中的方法。通常有两种验证方法:
在onBind中验证,验证不通过返回null
验证方式比如permission验证,在AndroidManifest声明:
<permission
android:name="com.rgy.chapter_2.permisson.ACCESS_BOOK_SERVICE"
android:protectionLevel="normal"/>
public IBinder onBind(Intent intent){
int check = checkCallingOrSelefPermission("com.ryq.chapter_2.permission.ACCESS_BOOK_SERVICE");
if(check == PackageManager.PERMISSION_DENIED){
return null;
}
return mBinder;
}
这种方法也适用于Messager。
在onTransact中验证,验证不通过返回false
可以permission验证,还可以采用Uid和Pid验证。
2.4.5 使用ContentProvider
==ContentProvider是四大组件之一,天生就是用来进程间通信。和Messenger一样,其底层实现是用Binder。==
系统预置了许多ContentProvider,比如通讯录、日程表等。要RPC访问这些信息,只需要通过ContentResolver的query、update、insert和delete方法即可。
创建自定义的ContentProvider,只需继承ContentProvider类并实现 onCreate 、 query 、 update 、 insert 、 getType 六个抽象方法即可。getType用来返回一个Uri请求所对应的MIME类型,剩下四个方法对应于CRUD操作。这六个方法都运行在ContentProvider进程中,除了 onCreate 由系统回调并运行在主线程里,其他五个方法都由外界调用并运行在Binder线程池中。
ContentProvider是通过Uri来区分外界要访问的数据集合,例如外界访问ContentProvider中的表,我们需要为它们定义单独的Uri和Uri_Code。根据Uri_Code,我们就知道要访问哪个表了。
==query、update、insert、delete四大方法存在多线程并发访问,因此方法内部要做好线程同步。==若采用SQLite并且只有一个SQLiteDatabase,SQLiteDatabase内部已经做了同步处理。若是多个SQLiteDatabase或是采用List作为底层数据集,就必须做线程同步。
2.4.6 使用Socket
Socket也称为“套接字”,分为流式套接字和用户数据报套接字两种,分别对应于TCP和UDP协议。Socket可以实现计算机网络中的两个进程间的通信,当然也可以在本地实现进程间的通信。我们以一个跨进程的聊天程序来演示。
在远程Service建立一个TCP服务,然后在主界面中连接TCP服务。服务端Service监听本地端口,客户端连接指定的端口,建立连接成功后,拿到 Socket 对象就可以向服务端发送消息或者接受服务端发送的消息。
除了采用TCP套接字,也可以用UDP套接字。实际上socket不仅能实现进程间的通信,还可以实现设备间的通信(只要设备之间的IP地址互相可见)。
2.5 Binder连接池
前面提到AIDL的流程是:首先创建一个service和AIDL接口,接着创建一个类继承自AIDL接口中的Stub类并实现Stub中的抽象方法,客户端在Service的onBind方法中拿到这个类的对象,然后绑定这个service,建立连接后就可以通过这个Stub对象进行RPC。
那么如果项目庞大,有多个业务模块都需要使用AIDL进行IPC,随着AIDL数量的增加,我们不能无限制地增加Service,我们需要把所有AIDL放在同一个Service中去管理。
服务端只有一个Service,把所有AIDL放在一个Service中,不同业务模块之间不能有耦合
服务端提供一个 queryBinder 接口,这个接口能够根据业务模块的特征来返回响应的Binder对象给客户端
不同的业务模块拿到所需的Binder对象就可以进行RPC了
2.6 选用合适的IPC方式
/////////----------------
一 Activity
1 Activity 生命周期
1.1 Activity 的四种状态
running 当前Activity正在运行,获取焦点
paused 当前Activity处于暂停状态,可见,没有焦点
stopped 当前Activity处于暂停状态,完全不可见,内存里的成员变量和状态信息仍在。
killed 当前Activity被销毁后的状态,成员变量和状态信息被一并回收。
1.2 Activity的生命周期
Activity启动 →onCreate()→onStart()→onResume();
点击home键返回桌面→onPause()→onStop();
再次回到原Activity→ onRestart()→onStart()→onResume();
按返回键退出当前Activity→onPause()→onStop()→onDestroy();
2 Android任务栈
优先级:前台>可见>服务>后台>空
前台:正在与用户进行交互的Activity所在的进程
可见:Activity可见但没有在前台所在的进程
服务:Activity在后台开启了服务所在的进程
后台:Activity完全处于后台所在的进程
空:没有任何Activity存在的进程
3. Activity的启动模式
3.1 为什么需要启动模式?
每次启动一个Activity都会把对应的要启动的Activity的实例放入任务栈中,加入这个Activity被频繁启动,会产生很多的这个Activity的实例,为了杜绝这种内存浪费的行为,Activity的启动模式被创造出来。
3.2 Activity的启动模式
系统模式模式:standard
标准模式,也是系统的默认模式,启动一个activity就创建一个activity实例,不管这个实例是否存在,谁启动了这个Activity,那么这个Activity就运行在启动它的那个Activity的任务栈中。
栈顶复用模式:singleTop
在这种模式下,如果新的Activity已经位于栈顶,那么此Activity不会被重新创建,同时它的onNewIntent方法被回调,通过此方法的参数我们可以取出当前的请求信息。需要注意,此Activity的onCreate,onStart方法不会被系统调用。如果新Activity不在栈顶,那么新Activity任然会被重新重建。
栈内复用模式:singleTask
这是一种单实例模式,只要Activity在一个栈中存在,那么多次启动此Activity都不会重新创建实例,系统也会回调onNewIntent方法。
例如:当前栈内情况为ABC,此时D被以singleTask的模式被启动,当前栈变为ABCD。
如果当前栈内情况为ADBC,此时D被以singleTask的模式被启动,当前栈变为AD。
单实例模式:singleInstance
这是一种加强的单实例模式,它除了具有singleTask模式的所有特性外,还加强了一点,那就是具有此种模式的Activity只能单独位于一个任务栈中,比如Activity A是singleInstance模式,A被启动时系统会为它创建一个新的任务栈,A运行在这个单独的任务栈中,后续的请求均不会再创建A,除非这个单独的任务栈被系统销毁了。
二 Fragment
1. 为什么Fragment被称为第五大组件?
Android中的四大组件为Activity,service,ContentProvider,Broadcast。
Fragment因为有生命周期,使用频率不输于四大组件,可灵活加载到Activity中。
1.1 Fragment加载到Activity的两种方式
静态加载:直接在Activity布局文件中指定Fragment。代码如下
<fragment
android:name="com.example.myfragment.MyFragment"
android:id="@+id/myfragment_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
动态加载:动态加载需要使用到FragmentManager,这种加载方式在开发中是非常常见的,示例代码如下:
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();//将FragmentA从容器中移除掉,减少内存的消耗fragmentTransaction.remove(fragmentA);
fragmentTransaction.add(R.id.fragment_layout,new FragmentB());
fragmentTransaction.commit();
1.2 Fragment 与ViewPager搭配使用
通常情况下我们开发应用最常见的使用情况是TabLayout+ViewPager+Fragment的使用方式,这就涉及到两个常用的适配器的使用,一个是FragmentPagerAdapter,另外一个是FragmentStatePagerAdapter,那么它们之间有什么区别呢?其实很简单,FragmentPagerAdapter适用于页面较少的情况,而FragmentStatePagerAdapter适用于页面较多的情况。
2. Fragment的生命周期
Fragment
界面打开
onCreate() 方法执行!
onCreateView() 方法执行!
onActivityCreated() 方法执行!
onStart() 方法执行!
onResume() 方法执行!
按下主屏幕键/锁屏
onPause() 方法执行!
onStop() 方法执行!
重新打开
onStart() 方法执行!
onResume() 方法执行!
按下后退键
onPause() 方法执行!
onStop() 方法执行!
onDestroyView() 方法执行!
onDestroy() 方法执行!
onDetach() 方法执行!
Activity
打开应用
onCreate() 方法执行!
onStart() 方法执行!
onResume() 方法执行!
按下主屏幕键/锁屏
onPause() 方法执行!
onStop() 方法执行!
重新打开应用
onRestart() 方法执行!
onStart() 方法执行!
onResume() 方法执行!
按下后退键
onPause() 方法执行!
onStop() 方法执行!
onDestroy() 方法执行!
在Activity中加入Fragment,对应的生命周期
打开
Fragment onAttach()方法执行
Fragment onCreate() 方法执行!
Fragment onCreateView() 方法执行!
Fragment onViewCreated()方法执行
Activity onCreate() 方法执行!
Fragment onActivityCreated() 方法执行!
Activity onStart() 方法执行!
Fragment onStart() 方法执行!
Activity onResume() 方法执行!
Fragment onResume() 方法执行!
按下主屏幕键/锁屏
Fragment onPause() 方法执行!
Activity onPause() 方法执行!
Fragment onStop() 方法执行!
Activity onStop() 方法执行!
再次打开
Activity onRestart() 方法执行!
Activity onStart() 方法执行!
Fragment onStart() 方法执行!
Activity onResume() 方法执行!
Fragment onResume() 方法执行!
按下后退键
Fragment onPause() 方法执行!
Activity onPause() 方法执行!
Fragment onStop() 方法执行!
Activity onStop() 方法执行!
Fragment onDestroyView() 方法执行!
Fragment onDestroy() 方法执行!
Fragment onDetach() 方法执行!
Activity onDestroy() 方法执行!
3. Fragment的通信
3.1 在Fragment中调用Activity中的方法
在Fragment中调用Activity的方法很简单,Fragment有个getActivity()的方法,比如,在MainActivity中的一个Fragment中获取MainActivity的引用,并调用MainActivity的某个方法methodA()方法你可以这么写:
MainActivity mainActivity = (MainActivity) getActivity();
mainActivity.methodA();
3.2 在Activity中调用Fragment的方法
在Activity中调用Fragment中的方法是最简单的,我想这里我不用多说吧!直接接口回调即可调用Fragment的任何可访问的方法。
3.3 在Fragment中调用另外一个Fragment的方法
这个可就需要一定的思维性了,首先要想调用Fragment A的方法,除了这个Fragment A自身可以调用外,这个Fragment A所属的Activity也可以调用,要想另外一个Fragment B调用此Fragment A的方法,Fragment B可以间接通过Activity来进行调用,也就是3.1 和 3.2 的结合。
三 Service
1. Service基础知识
1.1 Service是什么?
Service(服务)是一个一种可以在后台执行长时间运行操作而没有用户界面的组件。它运行于UI线程,因此不能进行耗时的操作。
1.2 Service和Thread的区别
Service的运行是在UI线程当中的,是绝对绝对不能进行耗时操作的,而Thread开启的子线程则可以进行耗时操作,但是Thread开启的子线程是不能直接对UI进行操作的,否则极有可能发生直接让程序崩掉,这就是它们的区别。
2. 启动Service的2种方式
2.1 startService()方法开启Service
步骤:
a.定义一个类继承Service。
b.在AndroidManifest.xml文件中配置该Service。
c.使用Context的startService(Intent)方法启动该Service。
d.不再使用该Service时,调用Context的stopService(Intent)方法停止该Service。
2.2 bindService方法开启Service(Activity与Service绑定)
步骤:
a.创建BinderService服务端,继承自Service并在类中创建一个实现IBinder接口的实现实例对象并提供公共方法给客户端调用。
b.从onBind()回调方法返回此Binder实例。
c.在客户端中,从onServiceConnected回调方法接收Binder,并使用提供的方法调用绑定服务。
3. Service的生命周期
服务的生命周期有两种,因为服务可以跟Activity绑定起来,也可以不绑定,Activity和服务进行通信的话,是需要把服务和Activity进行绑定的。因此服务的生命周期分为未绑定Activity的和绑定Activity的。
没有绑定Activity的服务生命周期:
启动服务>onCreate()>onStartCommand()>服务运行>onDestory()>服务销毁
绑定Activity的服务生命周期
绑定服务>onCreate()>onBind()>服务运行>onUnBind()>onDestory()>服务被销毁
通过Intent和startService()方法启动了一个服务,接下来执行onCreate()方法,首次创建服务时,系统将调用此方法来执行一次性设置程序(在调用 onStartCommand() 或 onBind() 之前)。如果服务已在运行,则不会调用此方法。
当另一个组件(如 Activity)通过调用 startService() 请求启动服务时,系统将调用此方法。一旦执行此方法,服务即会启动并可在后台无限期运行。 如果您实现此方法,则在服务工作完成后,需要由您通过调用 stopSelf() 或 stopService() 来停止服务。(如果您只想提供绑定,则无需实现此方法。)
服务开始处于运行状态。
某个操作导致服务停止,比如执行了方法stopService(),那么服务接下来会执行onDestory()销毁。服务应该实现此方法来清理所有资源,如线程、注册的侦听器、接收器等。 这是服务接收的最后一个调用。
服务被完全销毁,下一步就是等待被垃圾回收器回收了。
通过Intent和bindService()方法启动了一个服务,接下来会执行onCreate()方法,首次创建服务时,系统将调用此方法来执行一次性设置程序(在调用 onStartCommand() 或 onBind() 之前)。如果服务已在运行,则不会调用此方法。
当另一个组件想通过调用 bindService() 与服务绑定(例如执行 RPC)时,系统将调用此方法。在此方法的实现中,您必须通过返回 IBinder 提供一个接口,供客户端用来与服务进行通信。请务必实现此方法,但如果您并不希望允许绑定,则应返回 null。
服务开始处于运行状态。成功与Activity绑定。
某个操作导致服务解除绑定,比如执行了方法unbindService(),那么服务接下来会解除与当前Activity的绑定。接下来服务将面临销毁。
服务执行onDestory()方法被销毁。服务应该实现此方法来清理所有资源,如线程、注册的侦听器、接收器等。 这是服务接收的最后一个调用。
服务被完全销毁,下一步就是等待被垃圾回收器回收了。
Service总结:
被启动的服务的生命周期:如果一个Service被某个Activity 调用 Context.startService 方法启动,那么不管是否有Activity使用bindService绑定或unbindService解除绑定到该Service,该Service都在后台运行。如果一个Service被startService 方法多次启动,那么onCreate方法只会调用一次,onStart将会被调用多次(对应调用startService的次数),并且系统只会创建Service的一个实例(因此你应该知道只需要一次stopService调用)。该Service将会一直在后台运行,而不管对应程序的Activity是否在运行,直到被调用stopService,或自身的stopSelf方法。当然如果系统资源不足,android系统也可能结束服务。
被绑定的服务的生命周期:如果一个Service被某个Activity 调用 Context.bindService 方法绑定启动,不管调用 bindService 调用几次,onCreate方法都只会调用一次,同时onStart方法始终不会被调用。当连接建立之后,Service将会一直运行,除非调用Context.unbindService 断开连接或者之前调用bindService 的 Context 不存在了(如Activity被finish的时候),系统将会自动停止Service,对应onDestroy将被调用。
被启动又被绑定的服务的生命周期:如果一个Service又被启动又被绑定,则该Service将会一直在后台运行。并且不管如何调用,onCreate始终只会调用一次,对应startService调用多少次,Service的onStart便会调用多少次。调用unbindService将不会停止Service,而必须调用 stopService 或 Service的 stopSelf 来停止服务。
当服务被停止时清除服务:当一个Service被终止(1、调用stopService;2、调用stopSelf;3、不再有绑定的连接(没有被启动))时,onDestroy方法将会被调用,在这里你应当做一些清除工作,如停止在Service中创建并运行的线程。
四 Broadcast
1. 广播的概念
1.1 定义
在Android中,它是一种广泛运用在应用程序之间传输信息的机制,Android中我们发送广播内容是一个Intent,这个Intent中可以携带我们要发送的数据。
1.2 广播的使用场景
a.同一app内有多个进程的不同组件之间的消息通信。
b.不同app之间的组件之间消息的通信。
1.3 广播的种类
标准广播:context.sendBroadcast(Intent)方法发送的广播,不可被拦截
有序广播:context.sendOrderBroadcast(Intent)方法发送的广播,可被拦截
本地广播:localBroadcastManager.sendBroadcast(Intent),只在app内传播
2. 广播接收器
广播接收器是专门用来接收广播信息的,它可分为静态注册和动态注册:
静态注册:注册完成一直在运行。
首先你要创建一个广播接收器类,实例代码如下:
public class BootCompleteReceiver extends BroadcastReceiver {
@Override public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "Boot Complete", Toast.LENGTH_LONG).show();
}
}
另外,静态的广播接收器一定要在AndroidManifest.xml文件中注册才可以使用,AndroidManifest.xml文件中注册静态广播代码如下:
<receiver
android:name=".BootCompleteReceiver" > <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED" /> </intent-filter> </receiver>
动态注册:跟随Activity的生命周期。
新建一个类,让它继承自BroadcastReceiver,并重写父类的onReceive()方法就行了。这样有广播到来时,onReceive()方法就会得到执行,具体的逻辑就可以在这个方法中处理。
动态注册广播接收器的优点以及缺点:
动态注册的广播接收器可以自由地控制注册与注销,在灵活性方面有很大优势,但是它也存在着一个缺点,即必须要在程序启动之后才能接收到广播,因为注册的逻辑是写在onCreate()方法中的。那么有没有广播能在程序未启动的情况下就能接收到广播呢?静态注册的广播接收器就可以做到。
3. 广播内部实现机制
自定义广播接收者BroadcastReceiver,并且重写onReceiver()方法。
通过Binder机制向AMS(Activity Manager Service)进行注册。
广播发送者通过Binder机制向AMS发送广播。
AMS查找符合条件(IntentFilter/Permission等)的BroadcastReceiver,将广播发送到相应的BroadcastReceiver(一般情况下是Activity)的消息队列中。
消息循环执行拿到此广播,回调BroadcastReceiver中的onReceiver()方法。
4. 本地广播
本地广播的发送和注册广播接收器都需要使用到LocalBroadcastManager类,如下所示为本地广播的发送和本地广播接收器注册的代码:
本地广播的发送:
public static void sendLocalBroadcast(Context context,String action){
Intent intent = new Intent(action);
LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(context);
localBroadcastManager.sendBroadcast(intent);
}
本地广播的接收器的注册:
IntentFilter intentFilter = new IntentFilter();
LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(context);
intentFilter.addAction(new BroadcastUtil().action_next);
nasbr = new NextAndStartBroadcastReceiver();
localBroadcastManager.registerReceiver(nasbr, intentFilter);//注册本地广播接收器
特点:
1. 使用它发送的广播将只在自身app内传播,因此你不必担心泄漏隐私的数据。
2. 其他app无法对你的app发送该广播,因此你的app根本不可能收到非自身app发送的该广播,因此你不必担心有安全漏洞可以利用。
3. 比系统广播更加高效。
内部实现机制:
1. LocalBroadcast高效的原因:因为它内部是通过Handler实现的,它的sendBroadcast()方法含义并非和系统的sendBroadcast()一样,它的sendBroadcast()方法其实就是通过Handler发送了一个Message而已。
2. LocalBroadcast安全的原因:既然它是通过Handler实现广播发送的,那么相比系统广播通过Binder机制实现那肯定更加高效,同时使用Handler来实现,别的app无法向我们应用发送该广播,而我们app内部发送的广播也不会离开我们的app。
LocalBroadcast内部协作主要是靠两个Map集合:mReceivers和mActions,当然还有一个List集合mPendingBroadcasts,这个主要存储待接收的广播对象。
六 Bainder机制
通常情况下,Binder是一种通信机制。
对于Server来说,Binder指的是Binder本地对象/对于Client来说,Binder指的是Binder的代理对象。
对于传输过程而言,Binder是可以进行跨进程传递的对象。
AIDL是Binder机制的一个实例。
七 Handler机制
1. 定义
Handler是可以通过发送和处理Message和Runnable对象来关联相应线程的MessageQueue。通常我们认为它是一种异步机制。
可以让对应的Message和Runnable在未来的某个时间点进行相应的处理。
让自己想要的耗时操作在子线程中完成,让更新UI的操作在主线程中完成,而子线程与主线程之间的通信就是靠Handler来完成。
2. Handler的使用方法
post(Runnable)
sendMessage(Message)
3. Handler内部实现机制
Handler机制也可叫异步消息机制,它主要由4个部分组成:Message,Handler,MessageQueue,Looper,在上面我们已经接触到了Message和Handler,接下来我们对4个成员进行着重的了解:
Message
Message是在线程之间传递的消息,它可以在内部携带少量的信息,用于在不同线程之间交换数据。使用Message的arg1和arg2便可携带int数据,使用obj便可携带Object类型数据。
Handler
Handler顾名思义就是处理者的意思,它只要用于在子线程发送消息对象Message,在UI线程处理消息对象Message,在子线程调用sendMessage方法发送消息对象Message,而发送的消息经过一系列地辗转之后最终会被传递到Handler的handleMessage方法中,最终在handleMessage方法中消息对象Message被处理。
MessageQueue
MessageQueue就是消息队列的意思,它只要用于存放所有通过Handler发送过来的消息。这部分消息会一直存放于消息队列当中,等待被处理。每个线程中只会有一个MessageQueue对象,请牢记这句话。其实从字面上就可以看出,MessageQueue底层数据结构是队列,而且这个队列只存放Message对象。
Looper
Looper是每个线程中的MessageQueue的管家,调用Looper的loop()方法后,就会进入到一个无限循环当中,然后每当MesssageQueue中存在一条消息,Looper就会将这条消息取出,并将它传递到Handler的handleMessage()方法中。每个线程只有一个Looper对象。
Handler机制流程图如下:
Handler机制流程图
4. Handler引起的内存泄漏以及解决方法
原因:静态内部类持有外部类的匿名引用,导致外部activity无法得到释放。
解决方法:handler内部持有外部的弱引用,并把handler改为静态内部类,在activity的onDestory()中调用handler的removeCallback()方法。
八 IntentService机制
1.IntentService是什么?
它的优先级高于Service。
IntentService是继承处理异步请求的一个类,在IntentService内有一个工作线程来处理耗时操作,启动IntentServiced的方式和启动传统的Service一样,同时,当任务执行完成后,IntentService会自动停止,而不需要我们手动去控制或stopSelf()。另外,可以启动IntentService多次,而每一个耗时操作会以工作队列的方式在IntentService的onHandlerIntent回调方法中执行,并且,每次只执行一个工作线程,执行完第一个在执行第二个。
它本质是一种特殊的Service,继承自Service并且本身就是一个抽象类。
它内部是由HandlerThread和Handler实现异步操作。
2.IntentService的使用方法
创建IntentService时,只需要实现onHandlerIntent和构造方法,onHandlerIntent为异步方法,可以执行耗时操作。
十 HandlerThread机制
1.HandlerThread的产生背景
开启子线程进行耗时操作,多次创建和销毁子线程是很耗费资源的,但是木有关系,谷歌考虑了这点为我们专门开发出了HandlerThread机制。
2.HandlerThread是什么?
本质:Handler + Thread + Looper,是一个Thread内部有Looper。
HandlerThread本质上是一个线程类,它继承了Thread。
HandlerThread有自己内部的Looper对象,可以进行Looper循环。
通过获取HandlerThread的Looper对象传递给Handler对象,可以在handlerMessage方法中执行异步任务。
优点是不会有堵塞,减少对性能的消耗,缺点是不能进行多任务的处理,需要等待进行处理,处理效率较低。
与线程池注重并发不同,HandlerThread是一个串行队列,HandlerThread背后只有一个线程。
十一 IntentService机制
1.IntentService是什么?
它的优先级高于Service。
IntentService是继承处理异步请求的一个类,在IntentService内有一个工作线程来处理耗时操作,启动IntentServiced的方式和启动传统的Service一样,同时,当任务执行完成后,IntentService会自动停止,而不需要我们手动去控制或stopSelf()。另外,可以启动IntentService多次,而每一个耗时操作会以工作队列的方式在IntentService的onHandlerIntent回调方法中执行,并且,每次只执行一个工作线程,执行完第一个在执行第二个。
它本质是一种特殊的Service,继承自Service并且本身就是一个抽象类。
它内部是由HandlerThread和Handler实现异步操作。
2.IntentService的使用方法
创建IntentService时,只需要实现onHandlerIntent和构造方法,onHandlerIntent为异步方法,可以执行耗时操作。
十二 View绘制机制
1. View树的绘制流程
measure(测量)→layout(布局)→draw(绘制)
2. Measure过程
Measure过程
measure过程主要就是从顶层父View向子View递归调用view.measure方法(measure中又回调onMeasure方法)的过程。具体measure核心主要有如下几点:
MeasureSpec(View的内部类)测量规格为int型,值由高2位规格模式specMode和低30位具体尺寸specSize组成。其中specMode只有三种值:
MeasureSpec.EXACTLY //确定模式,父View希望子View的大小是确定的,由specSize决定;
MeasureSpec.AT_MOST //最多模式,父View希望子View的大小最多是specSize指定的值;
MeasureSpec.UNSPECIFIED //未指定模式,父View完全依据子View的设计值来决定;
View的measure方法是final的,不允许重载,View子类只能重载onMeasure来完成自己的测量逻辑。
最顶层DecorView测量时的MeasureSpec是由ViewRootImpl中getRootMeasureSpec方法确定的(LayoutParams宽高参数均为MATCH_PARENT,specMode是EXACTLY,specSize为物理屏幕大小)。
ViewGroup类提供了measureChild,measureChild和measureChildWithMargins方法,简化了父子View的尺寸计算。
只要是ViewGroup的子类就必须要求LayoutParams继承子MarginLayoutParams,否则无法使用layout_margin参数。
View的布局大小由父View和子View共同决定。
使用View的getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值。
3. Layout过程
整个layout过程比较容易理解,从上面分析可以看出layout也是从顶层父View向子View的递归调用view.layout方法的过程,即父View根据上一步measure子View所得到的布局大小和布局参数,将子View放在合适的位置上。具体layout核心主要有以下几点:
View.layout方法可被重载,ViewGroup.layout为final的不可重载,ViewGroup.onLayout为abstract的,子类必须重载实现自己的位置逻辑。
measure操作完成后得到的是对每个View经测量过的measuredWidth和measuredHeight,layout操作完成之后得到的是对每个View进行位置分配后的mLeft、mTop、mRight、mBottom,这些值都是相对于父View来说的。
凡是layout_XXX的布局属性基本都针对的是包含子View的ViewGroup的,当对一个没有父容器的View设置相关layout_XXX属性是没有任何意义的(前面《Android应用setContentView与LayoutInflater加载解析机制源码分析》也有提到过)。
使用View的getWidth()和getHeight()方法来获取View测量的宽高,必须保证这两个方法在onLayout流程之后被调用才能返回有效值。
4.Draw过程
绘制过程就是把View对象绘制到屏幕上,整个draw过程需要注意如下细节:
如果该View是一个ViewGroup,则需要递归绘制其所包含的所有子View。
View默认不会绘制任何内容,真正的绘制都需要自己在子类中实现。
View的绘制是借助onDraw方法传入的Canvas类来进行的。
区分View动画和ViewGroup布局动画,前者指的是View自身的动画,可以通过setAnimation添加,后者是专门针对ViewGroup显示内部子视图时设置的动画,可以在xml布局文件中对ViewGroup设置layoutAnimation属性(譬如对LinearLayout设置子View在显示时出现逐行、随机、下等显示等不同动画效果)。
在获取画布剪切区(每个View的draw中传入的Canvas)时会自动处理掉padding,子View获取Canvas不用关注这些逻辑,只用关心如何绘制即可。
默认情况下子View的ViewGroup.drawChild绘制顺序和子View被添加的顺序一致,但是你也可以重载ViewGroup.getChildDrawingOrder()方法提供不同顺序。
十三 Android部分事件分发机制
1. 为什么有事件分发机制
Android上面的View是树形结构,View可能会重叠在一起,当我们点击的地方有多个View都可以响应的时候,这个点击事件应该给谁呢?为了解决这个问题,就有了事件分发机制。
2. 3个重要的有关事件分发的方法
dispatch TouchEvent
用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级的dispatchTouchEvent方法影响,表示是否消耗此事件。
onInterceptTouchEvent
在上述方法dispatchTouchEvent内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
onTouchEvent
同样也会在dispatchTouchEvent内部调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。
伪代码
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;//记录返回值 if(onInterceptTouchEvent(ev)){//判断是否拦截此事件 consume = onTouchEvent(ev);//如果当前确认拦截此事件,那么就处理这个事件 }else{
consume = child.dispatchToucnEvent(ev);//如果当前确认不拦截此事件,那么就将事件分发给下一级 }
return consume;
}
通过上述伪代码,我们可以得知点击事件的传递规则:对于一个根ViewGroup而言,点击事件产生后,首先会传递给它,这时它的dispatchTouch就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前的事件,接着事件就会交给这个ViewGroup处理,即它的onTouch方法就会被调用;如果这个ViewGroup的onInterceptTouchEvent方法返回false就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法就会被调用,如此直到事件被最终处理。
当一个View需要处理事件时,如果它设置了OnTouchListener,那么OnTouchListener中的onTouch方法会被回调。这时事件处理还要看onTouch的返回值,如果返回false,则当前View的onTouchEvent方法会被调用;如果返回true,那么当前View的onTouchEvent方法不会被调用。由此可见,给View设置的onTouchListener的优先级比onTouchEvent要高。在onTouchEvent方法中,如果当前设置的有onClickListener,那么它的onClick方法会被调用。可以看出,平时我们常用的OnClickListener,其优先级最低,即处于事件传递的尾端。
当一个点击事件产生后,它的传递过程遵循如下顺序:Activity–>Window–>View,即事件总数先传递给Activity,Activity再传递给Window,最后Window再传递给顶级View,顶级View接收到事件后,就会按照事件分发机制去分发事件。考虑一种情况,如果一个View的onTouchEvent返回false,那么它的父容器的onTouchEvent将会被调用,依次类推。如果所有的元素都不处理这个事件,那么这个事件将会最终传递给Activity处理, 即Activity的onTouchEvent方法会被调用。这个过程其实很好理解,我们可以换一种思路,假设点击事件是一个难题,这个难题最终被上级领导分给了一个程序员去处理(这是事件分发过程),结果这个程序员搞不定(onTouchEvent返回了false),现在该怎么办呢?难题必须要解决,那就只能交给水平更高的上级解决(上级的onTouchEvent被调用),如果上级再搞不定,那就只能交给上级的上级去解决,就这样难题一层层地向上抛,这是公司内部一种常见的处理问题的过程。
关于事件传递机制还需要注意以下:
同一见事件序列是从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件的序列以down开始,中间含有数量不定的move事件,最终以up事件结束。
正常情况下,一个事件序列只能被一个View拦截且消耗。这一条的原因可以参考(3),因为一旦一个元素拦截了某个事件,那么同一个事件序列的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别由两个View同时处理,但是通过特殊手段可以做到,比如
一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理。
某个View一旦决定拦截,那么这个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的onInterceptTouchEvent不会被调用。这条也很好理解,就是说当一个View决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不用再调用这个View的onInterceptTouchEvent去询问它是否拦截了。
某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一件序列中的其他事件都不会再交给它处理,并且事件 将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。意思就是事件一旦交给一个View处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它处理了,这就好比上级交给程序员一件事,如果这件事没有处理好,短时间内上级就不敢再把事件交给这个程序员做了,二者是类似的道理。
如果View不消耗ACTION_DOWN以外的事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。
ViewGroup默认不拦截任何事件。Android源码中ViewGroup的onInterceptTouchEvent方法默认返回false。
View没有onInterceptTouchEvent方法,一旦点击事件传递给它,那么它的onTouchEvent方法就会被调用。
View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable属性默认为false,clickable属性要分情况,比如Button的clickable属性默认为true,而TextView的clickable属性默认为false。
View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true。
onClick会发生的前提是当前View是可点击的,并且它接收到了down和up事件。
事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。
3. 事件分发的流程
Activity–>PhoneWindow–>DecorView–>ViewGroup–>…–>View(View树最底部的View)
事件分发
十四 动画
1.Android动画的分类
1.1 补间动画
a.渐变动画支持四种类型:平移(Translate)、旋转(Rotate)、缩放(Scale)、不透明度
b. 只是显示的位置变动,View的实际位置未改变,表现为View移动到其他地方,点击事件仍在原处才能响应。
c. 组合使用步骤较复杂。
d. View Animation 也是指此动画。
1.2 帧动画
a. 用于生成连续的Gif效果图。
b. DrawableAnimation也是指此动画
1.3 属性动画
a.支持对所有View能更新的属性的动画(需要属性的setXxx()和getXxx())。
b. 更改的是View实际的属性,所以不会影响其在动画执行后所在位置的正常使用。
c. Android3.0(API11)及以后出现的功能,3.0之前的版本可使用github第三方开源库nineoldandroids.jar进行支持。
2.补间动画,帧动画,属性动画优缺点
2.1 补间动画优缺点
缺点:当平移动画执行完停在最后的位置,结果焦点还在原来的位置(控件的属性没有真的被改变)
优点:相对于逐帧动画来说,补间动画更为连贯自然
2.2 帧动画优缺点
缺点:效果单一,逐帧播放需要很多图片,占用控件较大
优点:制作简单
2.3 属性动画优缺点
缺点:(3.0+API出现)向下兼容问题。
优点:易定制,效果强。
十五 自定义View
1. 自定义View的几种方式
对原View进行扩展方式
多个View的组合方式
重写View的方式
2.自定义View需要重写的方法。
onMesure(测量)
onLayout(布局)
onDraw(绘制)
十六 Context
Context的中文翻译为:语境; 上下文; 背景; 环境,在开发中我们经常说称之为“上下文”,那么这个“上下文”到底是指什么意思呢?在语文中,我们可以理解为语境,在程序中,我们可以理解为当前对象在程序中所处的一个环境,一个与系统交互的过程。比如微信聊天,此时的“环境”是指聊天的界面以及相关的数据请求与传输,Context在加载资源、启动Activity、获取系统服务、创建View等操作都要参与。
Context提供了关于应用环境全局信息的接口。它是一个抽象类,它的执行被Android系统所提供。它允许获取以应用为特征的资源和类型,是一个统领一些资源(应用程序环境变量等)的上下文。就是说,它描述一个应用程序环境的信息(即上下文);是一个抽象类,Android提供了该抽象类的具体实现类;通过它我们可以获取应用程序的资源和类(包括应用级别操作,如启动Activity,发广播,接受Intent等)。
Context.png
Context的作用域:
Context的作用域.png
十七 ContentProvider
1. 简介
内容提供者(Content Provider)主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访数据的安全性。目前,使用内容提供者是Android实现跨程序共享数据的标准方式。
不同于文件存储和SharedPreferences存储中的两种全局可读可写操作模式,内容提供者可以选择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会泄露的风险。
2.通过内容提供者访问其他程序的数据
内容提供者的用法一般有两种,一种是使用现有的内容提供器来读取和操作相应程序中的数据,另一种是创建自己的内容提供者来给我们的程序提供外部访问接口。如果一个程序通过内容提供者对其数据提供外部访问接口,那么任何其他的应用程序就都可以对这部分数据进行访问。Android系统中自带的电话簿,短信,媒体库等程序都提供了类似的访问接口,这就使得第三方应用程序可以充分利用这部分数据来实现更好的功能。下面我们就来看看如何通过内容提供者访问其他程序的数据:
2.1 ContentResolver的基本用法
想要访问内容提供者中共享的数据,就一定要借助CotentResolver类,可以通过Context中的getContentResolver()方法获取该类的实例。ContentResolver中提供了一系列的方法用于对数据进行CRUD(增删改查)操作,其中insert()方法用于添加数据,update()方法用于数据的更新,delete()方法用于数据的删除,query()方法用于数据的查询。这好像SQLite数据库操作有木有?
不同于SQLiteDatabase,ContentResolver中的增删改查方法都是不接收表名参数的,而是使用一个Uri的参数代替,这个参数被称作内容URI。内容URI给内容提供者中的数据建立了唯一的标识符,它主要由两部分组成:authority和path。authority是用于对不同的应用程序做区分的,一般为了避免冲突,都会采用程序包名的方式来进行命名。比如某个程序的包名为com.example.app,那么该程序对应的authority就可以命名为com.example.app.provider。path则是用于对同一应用程序中不同的表做区分的,通常都会添加到authority的后面。比如某个程序的数据库里存在两张表:table1和table2,这时就可以将path分别命名为/table1和/table2,然后把authority和path进行组合,内容的URI就变成了com.example.app.provider/table1和com.example.app.provider/table2。不过目前还是很难辨认出这两个字符串就是两个内容URI,我们还需要在字符串的头部加上协议声明。因此,内容URI最标准的格式写法如下:
content://com.example.app.provider/table1content://com.example.app.provider/table2
在得到内容URI字符串之后,我们还需要将它解析成Uri对象才可以作为参数传入。解析的方法也相当简单,代码如下所示:
Uri uri = new Uri.parse("content://com.example.app.provider/table1");
只需要调用Uri的静态方法parse()就可以把内容URI字符串解析成URI对象。
现在,我们可以通过这个Uri对象来查询table1表中的数据了。代码如下所示:
Cursor cursor = getContentResolver()
.query(
uri,projection,selection,selectionArgs,sortOrder
);
query()方法接收的参数跟SQLiteDatabase中的query()方法接收的参数很像,但总体来说这个稍微简单一些,毕竟这是在访问其他程序中的数据,没必要构建复杂的查询语句。下标对内容提供者中的query的接收的参数进行了详细的解释:
查询完成仍然会返回一个Cursor对象,这时我们就可以将数据从Cursor对象中逐个读取出来了。读取的思路仍然是对这个Cursor对象进行遍历,然后一条一条的取出数据即可,代码如下:
if(cursor != null){//注意这里一定要进行一次判空,因为有可能你要查询的表根本不存在 while(cursor.moveToNext()){
String column1 = cursor.getString(cursor.getColumnIndex("column1"));
int column2 = cursor.getInt(cursor.getColumnIndex("column2"));
}
}
增加,删除,修改
//增加数据ContentValues values = new ContentValues();
values.put("Column1","text");
values.put("Column2","1");
getContextResolver.insert(uri,values);//删除数据getContextResolver.delete(uri,"column2 = ?",new String[]{ "1" });//更新数据ContentValues values = new ContentValues();
values.put("Column1","改数据");
getContextResolver.update(uri,values,"column1 = ? and column2 = ?",new String[]{"text","1"});
3. 创建自己的内容提供者
前面已经提到过,如果要想实现跨程序共享数据的功能,官方推荐的方式就是使用内容提供器,可以新建一个类去继承ContentProvider类的方式来创建一个自己的内容提供器。ContentProvider类有6个抽象方法,我们在使用子类继承它的时候,需要将这6个方法全部重写。新建MyProvider继承字ContentProvider类,代码如下所示:
public class MyProvider extends ContentProvider {
@Override public boolean onCreate() {
return false;
}
@Override public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
return null;
}//查询 @Override public Uri insert(Uri uri, ContentValues values) {
return null;
}//添加 @Override public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
return 0;
}//更新 @Override public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}//删除 @Override public String getType(Uri uri) {
return null;
}
}
onCreate()方法:
初始化内容提供器的时候调用。通常会在这里完成对数据库的创建和升级等操作。返回true表示内容提供器初始化成功,返回false则表示失败。注意,只有当存在ContentResolver尝试访问我们的程序中的数据时,内容提供器才会被初始化。
query()方法:
从内容提供器中查询数据。使用uri参数来确定查询的哪张表,projection参数用于确定查询的哪一列,selection和selectionArgs参数用于约束查询哪些行,sortOrder参数用于对结果进行排序,查询的结果存放在Cursor对象中返回。
insert()方法:
向内容提供器中添加一条数据。使用uri参数来确定要添加的表,待添加的数据保存在values参数中。添加完成后,返回一个用于表示这条新纪录的URI。
update()方法:
更新内容提供器中已有的数据。使用uri参数来确定更新哪一张表中的数据,新数据保存着values参数当中,selection和selectionArgs参数用于约束更新哪些行,受影响的行数将作为返回值返回。
delete()方法:
从内容提供器中删除数据。使用uri参数来确定删除哪一张表中的数据,selection和selectionArgs参数用于约束删除哪些行,被删除的行数将作为返回值返回。
getType()方法:
根据传入的内容URI来返回相应的MIME类型。