《Android开发艺术探索》笔记
本笔记整理自: https://www.gitbook.com/book/tom510230/android_ka_fa_yi_shu_tan_suo/details
参考文章:http://szysky.com/tags/#笔记、http://blog.csdn.net/player_android/article/category/6577498
联系我:[email protected]
本书是一本Android进阶类书籍,采用理论、源码和实践相结合的方式来阐述高水准的Android应用开发要点。本书从三个方面来组织内容。
- 介绍Android开发者不容易掌握的一些知识点
- 结合Android源代码和应用层开发过程,融会贯通,介绍一些比较深入的知识点
- 介绍一些核心技术和Android的性能优化思想
目录
第1章 Activity的生命周期和启动模式
第2章 IPC机制
第3章 View的事件体系
第4章 View的工作原理
第5章 理解RemoteViews
第6章 Android的Drawable
第7章 Android动画深入分析
第8章 理解Window和WindowManager
第9章 四大组件的工作过程
第10章 Android的消息机制
第11章 Android的线程和线程池
第12章 Bitmap的加载和Cache
第13章 综合技术
第14章 JNI和NDK编程
第15章 Android性能优化
[TOC]
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的语法:
Intent中的data有两部分组成:mimeType和URI。mimeType是指媒体类型,比如
image/jpeg、audio/mpeg4-generic和video/等,可以表示图片、文本、视频等不同的媒
体格式。
URI的结构:
://:/[||]
实际例子
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 ListqueryIntentActivityies(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,并且会出现在系统的应用列表中,二者缺一不可。
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 CREATOR = new Creator() {
@Override
public Book createFromParcel(Parcel in) {
return new Book(in);
}
@Override
public 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
它内部通过一个Map接口来保存所有的AIDL回调,这个Map的key是 IBinder 类型,value是 Callback 类型。当客户端解除注册时,遍历服务端所有listener,找到和客户端listener具有相同Binder对象的服务端listenr并把它删掉。
==客户端RPC的时候线程会被挂起,由于被调用的方法运行在服务端的Binder线程池中,可能很耗时,不能在主线程中去调用服务端的方法。==
权限验证
默认情况下,我们的远程服务任何人都可以连接,我们必须加入权限验证功能,权限验证失败则无法调用服务中的方法。通常有两种验证方法:
- 在onBind中验证,验证不通过返回null
验证方式比如permission验证,在AndroidManifest声明:
Android自定义权限和使用权限
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了
BinderPool源码