IPC相关概念
IPC是Inter-Process-Communication的缩写,即进程间通信。Binder是Android中最具特色的进程间通信方式。
- 多进程引用方式
Android中引用多进程的唯一方式:对四大组件在AndroidMenifest.xml中指定android:process属性。
没有指定android:process属性就是默认进程,进程名为包名。
两种进程名写法:
android:process=": test",该进程名前附上包名即为其完整进程名,即com.zjrb.sjzsw: test。表示该进程为应用私有,不可共享。
android:process="com.zjrb.sjzsw.test",该名即为完整进程名,属于全局进程,其他组件可通过ShareUID方式共享进程。
- 序列化和反序列化
Serializable是Java提供的标准序列化接口,可在类的声明中指定标识(静态常量)用于反序列化时标识是同一个数据源。
//推荐手动指定serialVersionUID值,避免类结构改变引起hash值改变,进而反序列化失败。
private static final long serialVersionUID = 8711368828010083044L;
序列化和反序列化的过程即为将对象实例的数据写入文件和从文件读取的过程。原理同下:
静态变量属于类不属于对象实例,不参与序列化过程;用transient关键字标记的变量不参与序列化过程。
Parcelable和Serializable的区别:
- Serializable是Java中的序列化接口,其使用起来简单但是开销很大,序列化和反序列化过程中需要大量的I/O操作。
- Parcelable是Android中的序列化接口,更适合运用在Android平台上,缺点是使用起来麻烦些,但效率高。
Parcelable主要运用在内存序列化上,Serializable将对象序列化到存储设备中或者将序列化后通过网络传输比较方便。
在AIDL进程间通信中,实体类序列化宜采用Parcelable,实测暂不支持Serializable方式的序列化。
Binder机制
- 进程隔离
进程之间无法直接进行交互的,操作系统为了保证自身的安全稳定性,将系统空间分为内核空间和用户空间。
内核空间是系统内核运行的空间,内核空间数据可共享。
用户空间是用户程序运行的空间,用户空间数据不共享。因此进程间通信是靠内核空间驱动的。
当 Client 向 Server 发起 IPC 请求时,Client 会先将请求数据从用户空间拷贝到内核空间。系统会将内核空间中的数据拷贝到 Server端用户空间的缓存中。这样就成功的将 Client 进程中的请求数据传递到了 Server 进程中。
- Binder内在原理
Binder是一种架构,提供了服务端接口、Binder驱动、客户端接口三个模块。
ServiceManager:是一个独立的进程,管理各种系统服务。客户端调用Service之前,会向ServiceManager查询该服务是否存在,若存在则返回该Service的引用。
Binder描述符:在Binder内部,其唯一标识由当前Binder的类名全路径表示。
onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags):
该方法运行在服务端的Binder线程池中,当客户端发起跨进程请求时,远程请求会在内核空间的驱动交由此方法来处理。
- code 服务端通过code确定客户端请求的目标方法;
- data 从data中取出目标方法所需的参数,如果有的话;
- reply 目标方法执行完毕后,会向reply中写入返回值,如果有的话;
- flags 表示是否需要阻塞等待返回结果,0或FLAG_ONEWAY。
Binder通信流程如下:
服务端Binder对象创建后(同时Binder驱动中也会创建一个Binder对象),内部会启动一个隐藏的线程,该线程会接收Binder驱动发送的消息,之后会执行Binder对象的onTransact()函数,并按照该函数的参数执行不同的服务代码。
客户端通过Binder驱动中的Binder对象,该对象名为mRemote,调用其transact()方法,向服务端发送消息,并挂起客户端当前线程,待接收到服务端执行完指定函数后的通知,客户端线程恢复唤醒状态。
为什么用Binder机制?
- 性能:Binder机制下数据拷贝只需一次,而管道、消息队列和socket都需要两次,共享内存不需要内存拷贝,略优于Binder。
- 稳定性:Binder基于C/S架构,双端独立解耦,而共享内存需要处理并发问题。
- 安全性:Android为每个应用程序分配了鉴别进程身份的UID,C端将指令发送S端,S端会根据权限控制执行策略。
进程间通信方式
Bundle
Bundle实现了Parcelable接口,支持activity/service/receiver在进程间传递数据(ContentProvider默认支持进程间通信),并通过Intent发送。
Bundle支持的数据类型就是进程间通信的数据类型,即数据实现序列化。
最简单的进程间数据传递方式,推荐。
文件共享
两个进程通过读写同一个文件来交换数据,需要处理好线程同步问题,避免并发冲突。
SharedPreferences是采用XML文件来存储键值对,是带有缓存的文件存储,不推荐在进程间传递数据。
适合对同步要求不高的场景。
Messenger
Messenger底层是通过封装AIDL实现进程间通信,通过带有Handler的客户端接收从服务端传回的消息。
优点:支持一对多通信;支持实时通信;无并发问题;
缺点:不支持RPC(远程过程调用);数据类型单一(仅支持Bundle支持的数据类型);
简单的消息传输,不支持RPC,适合于低并发的一对多即时通信的场景。
- 案例解析:客户端client给服务端发消息,服务端server收到消息后回复客户端。
客户端:
public class MessengerActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.ok).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.jinzifu.myserver.Messenger");
intent.setPackage("com.jinzifu.myserver");
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
}
});
}
ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//① 建立通信 客户端通过Messenger向服务端发消息
Messenger messenger = new Messenger(service);
Bundle bundle = new Bundle();
bundle.putString("fromClient", "这是客户端对你家人的问候。");
Message message = new Message();
message.what = 102;
message.setData(bundle);
//② 传递客户端Messenger,用于接收服务端传回的消息
message.replyTo = mMessenger;
try {
messenger.send(message);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
Messenger mMessenger = new Messenger(new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case 101:
Bundle bundle = msg.getData();
String content = bundle.getString("fromServer");
Log.d("jinzifu", "来自服务端的消息:" + content);
break;
}
}
});
}
- 建立通信 客户端通过Messenger向服务端发消息;
- 向服务端传递客户端带有Handler的Messenger,用于接收服务端传回的消息;
服务端:
public class MessengerService extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
return new Messenger(new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case 102:
Bundle bundle = msg.getData();
String content = bundle.getString("fromClient");
Log.d("jinzifu", "来自客户端的消息:" + content);
Messenger messenger = msg.replyTo;
Bundle bundle1 = new Bundle();
bundle1.putString("fromServer", "已收到,回敬与你。");
Message message = new Message();
message.what = 101;
message.setData(bundle1);
try {
//③ 获得客户端的Messenger,并回传消息
messenger.send(message);
} catch (RemoteException e) {
e.printStackTrace();
}
break;
}
}
}).getBinder();
}
}
- 获得客户端的Messenger,并回传消息;
Log日志
com.jinzifu.myserver D/jinzifu: 来自客户端的消息:这是客户端对你家人的问候。
com.jinzifu.myclient D/jinzifu: 来自服务端的消息:已收到,回敬与你。
AIDL
AIDL 即Android Interface definition language的缩写,是Android接口定义语言。是系统内部提供的一种快速实现Binder的工具而已,也可手动实现。在Android中常用的通信方式是基于AIDL的远程Service。
基于AIDL的远程Service是非常复杂的通信方式,需考虑线程阻塞、权限验证、死亡监听等。
AIDL支持的所有类型(未列类型暂不支持):
注意:
- AIDL中传递的接口,不是普通的接口,必须是AIDL接口。且接口中只支持方法,不支持声明静态常量。
- AIDL中自定义的Parcelable对象和AIDL对象必须要显式的import进来,不管是否在同一个包内。
- AIDL中除了基本数据类型,其他支持的类型的参数都要标注方向:in、out和inout。
- 所有的AIDL接口都继承于 android.os.IInterface接口。
interface BaseDataAidlInterface {
/**
* 类型示例,删除即可
*/
void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString,in List list);
}
in、out和inout的区别:
- in表示输入型参数,数据只能由客户端传向服务端,服务端对数据的修改不会影响到客户端。
- out表示输出型参数,数据只能从服务端传向客户端,即使客户端通过方法入参向服务端传入对象,值也为空的。
- inout表示输入输出型参数。
in/out标签允许Binder跳过编组(序列化、传输、接收和反序列化)步骤,以获得更好的性能。
ContentProvider
ContentProvider是Android提供的专门为不同应用进行数据共享的方式。主要以表格的形式组织数据,还支持图片、视频等文件数据。
优点:在数据源访问方面功能强大,支持一对多并发数据共享,可通过Call方法扩展其他操作。
缺点:可以理解为受约束的AIDL,主要提供数据源的CRUD(create read update delete)操作,要注意SQLite注入和目录遍历的漏洞。
Android封装好的进程间数据共享方式,推荐,不支持RPC。
- 案例解析:服务端操作数据库提供数据给客户端。
URI统一资源标识符:是标识ContentProvider中资源唯一性的符号。
Uri uri = Uri.parse("content://com.jzf.progress/book/1");
- content:主题名,是ContentProvider的URI前缀,系统规定的;
- com.jzf.progress:授权信息,ContentProvider的唯一标识符;
- book:表名,ContentProvider指向数据库中的具体表名;
- 1:记录,表中的某个记录,如果没指定,默认返回的是全部记录;
Android提供了用于操作Uri的工具类UriMatcher,使用UriMatcher.match(uri)对输入的Uri进行匹配,如果匹配成功就返回匹配码,匹配码是调用addURI()方法传入的第三个参数。
ContentResolver 内容解析器:ContentResolver提供了与ContentProvider一样的增删改查方法,统一管理不同ContentProvider与外部进程的通信。ContentProvider#notifyChange方法通知外界访问者ContentProvider中的数据有更新。
服务端
public class EventProvider extends ContentProvider {
private static final String AUTHORITY = "com.jinzifu.myserver.EventProvider";
//① 关联Uri和Uri_Code,识别外界要操作的表
private static final int EVENT_URI_CODE = 101;
private static final UriMatcher uriMathcher =
new UriMatcher(UriMatcher.NO_MATCH);
private static final String TAG = "jinzifu";
static {
uriMathcher.addURI(AUTHORITY, "event", EVENT_URI_CODE);
}
private Context mContext;
private SQLiteDatabase mSQLiteDatabase;
/**
* ① 一般用于创建数据库或升级等操作,外界调用getContentResolver()时回调。
* onCreate 运行在UI线程中,其他方法运行在binder线程中。
*
* @return fasle则表示provider创建失败
*/
@Override
public boolean onCreate() {
Log.d(TAG, "onCreate: EventProvider初始化");
mContext = getContext();
mSQLiteDatabase = new MySQLiteHelper(mContext, "",
null, 0).getWritableDatabase();
new Thread(new Runnable() {
@Override
public void run() {
//② 数据库操作不应该放在主线程,数据库操作命令需掌握
mSQLiteDatabase.execSQL("delete from "
+ MySQLiteHelper.TABLE_EVENT);
mSQLiteDatabase.execSQL("insert into "
+ MySQLiteHelper.TABLE_EVENT
+ " values(1,'jzf',100);");
}
}).start();
return true;
}
/**
* ⑤ 查询数据
*
* @param uri 根据uri查询数据库中对应表的数据
* @param projection 选择符合条件的列查询数据,传null则查询所有列
* @param selection 选择符合条件的行查询数据,传null则查询所有行
* @param selectionArgs 类似selection
* @param sortOrder 对查询结果排序,传null则为默认排序,也可无序
* @return 返回一个Cursor对象,用后须关闭,避免内存泄露
*/
@Nullable
@Override
public Cursor query(@NonNull Uri uri,
@Nullable String[] projection,
@Nullable String selection,
@Nullable String[] selectionArgs,
@Nullable String sortOrder) {
String table = getTableFormUri(uri);
if (TextUtils.isEmpty(table)) return null;
Log.d("jinzifu", "EventProvider开始查询");
return mSQLiteDatabase.query(table,
projection,
selection,
selectionArgs,
null,
null,
sortOrder);
}
private String getTableFormUri(Uri uri) {
switch (uriMathcher.match(uri)) {
case EVENT_URI_CODE:
return MySQLiteHelper.TABLE_EVENT;
}
return null;
}
/**
* 返回指定内容的媒体类型
*
* @param uri
* @return MIME类型 如图片、视频等,可为null
*/
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
/**
* ② 添加数据
*
* @param uri 根据uri插入数据库中对应表的数据
* @param values ContentValues内部使用HashMap存储数据的,
* key表示列名,value表示行名,如果value为空,在表中是空行,无内容。
* @return 返回这条数据的uri
*/
@Nullable
@Override
public Uri insert(@NonNull Uri uri,
@Nullable ContentValues values) {
//② 与其他方法均属于并发编程的,要做好线程同步??
//③ SQLiteDatabase内部对数据库的操作是有同步处理的,
// 但多个SQLiteDatabase对象对ConentProvider并发操作无同步处理。
String table = getTableFormUri(uri);
if (TextUtils.isEmpty(table)) return null;
mSQLiteDatabase.insert(table, null, values);
mContext.getContentResolver().notifyChange(uri, null);
Log.d(TAG, "insert: 插入数据成功");
return uri;
}
/**
* ③ 删除数据
*
* @param uri 根据uri删除数据库中对应表的数据
* @param selection 选择符合条件的行数据删除
* @param selectionArgs 类似selection
* @return 返回被删除的行数
*/
@Override
public int delete(@NonNull Uri uri,
@Nullable String selection,
@Nullable String[] selectionArgs) {
String table = getTableFormUri(uri);
if (TextUtils.isEmpty(table)) return 0;
int count = mSQLiteDatabase.delete(table,
selection,
selectionArgs);
if (count > 0) mContext.getContentResolver().notifyChange(uri, null);
return count;
}
/**
* ④ 更改数据
*
* @param uri 根据uri修改数据库中对应表的数据
* @param values 同insert中的ContentValues用法,若value为空,则会将原来的数据置空
* @param selection 选择符合条件的行数据修改
* @param selectionArgs 类似selection
* @return 返回更新的行数
*/
@Override
public int update(@NonNull Uri uri,
@Nullable ContentValues values,
@Nullable String selection,
@Nullable String[] selectionArgs) {
String table = getTableFormUri(uri);
if (TextUtils.isEmpty(table)) return 0;
int row = mSQLiteDatabase.update(table, values, selection, selectionArgs);
if (row > 0) mContext.getContentResolver().notifyChange(uri, null);
return row;
}
}
public class MySQLiteHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "provider_test.db";
private static final int DB_VERSION = 1;
public static final String TABLE_EVENT = "event";
public static final String CREATE_EVENT_TABLE =
"CREATE TABLE IF NOT EXISTS " + TABLE_EVENT
+ "(_id INTEGER PRIMARY KEY,name TEXT,count INTEGER)";
public MySQLiteHelper(@Nullable Context context,
@Nullable String name,
@Nullable SQLiteDatabase.CursorFactory factory,
int version) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_EVENT_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
客户端
public class ProviderActivity extends AppCompatActivity {
private static final String TAG = "jinzifu";
private Uri uri =
Uri.parse("content://com.jinzifu.myserver.EventProvider/event");
private ContentObserver mContentObserver;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.ok)
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ContentValues contentValues = new ContentValues();
//① 数据插入时就知道数据的列名,这是SQLite注入的漏洞点
contentValues.put("_id", 2);
contentValues.put("name", "jzf2");
contentValues.put("count", 101);
getContentResolver().insert(uri, contentValues);
}
});
//监听ContentProvider的数据变化回调
getContentResolver().registerContentObserver(
uri,
true,
mContentObserver = new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange, Uri uri) {
super.onChange(selfChange, uri);
Log.d(TAG, "onChange: 监听到provider数据变化");
Cursor cursor = getContentResolver().query(
uri,
new String[]{"name"},
null,
null,
null);
if (cursor == null) return;
while (cursor.moveToNext()) {
Log.d(TAG, "name: "
+ cursor.getString(
cursor.getColumnIndex("name")));
}
//① Cursor用后需要及时回收,Cursor.close()。
cursor.close();
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
getContentResolver()
.unregisterContentObserver(mContentObserver);
}
}
Log日志
com.jinzifu.myserver D/jinzifu: insert: 插入数据成功
com.jinzifu.myclient D/jinzifu: onChange: 监听到provider数据变化
com.jinzifu.myserver D/jinzifu: EventProvider开始查询
com.jinzifu.myclient D/jinzifu: name: jzf
com.jinzifu.myclient D/jinzifu: name: jzf2
Socket
Socket称为套接字,分为TCP协议的流式套接字和UDP协议的用户数据报套接字。进程间可以通过Socket实现端到端的通信,且Socket支持任意字节流。
优点:功能强大,可通过网络传输字节流,支持一对多并发实时通信。
缺点:实现细节稍微有点繁琐,不支持直接的RPC,适用于网络数据交换(即需要网络支持)。