在 上文 介绍过如何使用 Messenger 和 AIDL 进行跨进程通信,这一篇来讲一下如何用 ContentProvider 和 Socket 进行 IPC。
1. 使用 ContentProvider 进行 IPC
ContentProvider 是 Android 中专门用于不同应用间进行数据共享的方式,它的底层也是 Binder 。
-
创建一个自定义的 ContentProvider
我们只需要继承 ContentProvider 并且实现
onCreate()
,query()
,update()
,insert()
,delete()
,getType()
方法就可以了。这里面除了onCreate()
运行在系统的主线程中,其他 5 个方法都由外界回调并运行在 Binder 线程池中。然后在 BookProvider 中创建一个数据库,供外界查询。
show my code
/**
*
* 服务端的 ContentProvider
*
* Created by innovator on 2018/1/30.
*/
public class BookProvider extends ContentProvider {
private static final String TAG = "BookProvider";
public static final String AUTHORITY = "com.innovator.ipcserver.provider";
public static final Uri BOOK_CONTENT_URI = Uri.parse("content://"+AUTHORITY+"/book");
public static final Uri USER_CONTENT_URI = Uri.parse("content://"+AUTHORITY+"/user");
public static final int BOOK_URI_CODE = 0;
public static final int USER_URI_CODE = 1;
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
sUriMatcher.addURI(AUTHORITY,"book",BOOK_URI_CODE);
sUriMatcher.addURI(AUTHORITY,"user",USER_URI_CODE);
}
/**
* 根据 Uri 来获取要访问哪个表
* @param uri
* @return
*/
private String getTableName(Uri uri){
String table = null;
switch (sUriMatcher.match(uri)){
case BOOK_URI_CODE:
table = DbOpenHelper.BOOK_TABLE_NAME;
break;
case USER_URI_CODE:
table = DbOpenHelper.USER_TABLE_NAME;
break;
default:
break;
}
return table;
}
private Context mContext;
private SQLiteDatabase mDb;
@Override
public boolean onCreate() {
Log.i("TAG","onCreate,当前线程是:"+Thread.currentThread().getName().toString());
mContext = getContext();
//另开线程初始化数据库
new Thread(new Runnable() {
@Override
public void run() {
initProviderData();
}
}).start();
return true;
}
private void initProviderData(){
mDb = new DbOpenHelper(mContext).getWritableDatabase();
mDb.execSQL("delete from "+DbOpenHelper.BOOK_TABLE_NAME);
mDb.execSQL("delete from "+DbOpenHelper.USER_TABLE_NAME);
mDb.execSQL("insert into book values(3,'Android');");
mDb.execSQL("insert into book values(4,'ios');");
mDb.execSQL("insert into book values(5,'Html');");
mDb.execSQL("insert into user values(1,'jake','1');");
mDb.execSQL("insert into user values(2,'jasmine','0');");
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
Log.i("TAG","query,当前线程是:"+Thread.currentThread().getName().toString());
String tableName = getTableName(uri);
if(tableName == null){
throw new IllegalArgumentException("Unsupported URI:"+uri);
}
return mDb.query(tableName,projection,selection,selectionArgs,null,null,sortOrder,null);
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
Log.i("TAG","getType,当前线程是:"+Thread.currentThread().getName().toString());
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
Log.i("TAG","insert,当前线程是:"+Thread.currentThread().getName().toString());
String tableName = getTableName(uri);
if(tableName == null){
throw new IllegalArgumentException("Unsupported URI:"+uri);
}
mDb.insert(tableName,null,values);
//通知外界当前的ContentProvider 数据已经发生改变
mContext.getContentResolver().notifyChange(uri,null);
return uri;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
Log.i("TAG","delete,当前线程是:"+Thread.currentThread().getName().toString());
String tableName = getTableName(uri);
if(tableName == null){
throw new IllegalArgumentException("Unsupported URI:"+uri);
}
int count = mDb.delete(tableName,selection,selectionArgs);
if(count >0){
//通知外界当前的ContentProvider 数据已经发生改变
mContext.getContentResolver().notifyChange(uri,null);
}
return count;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
Log.i("TAG","update,当前线程是:"+Thread.currentThread().getName().toString());
String tableName = getTableName(uri);
if(tableName == null){
throw new IllegalArgumentException("Unsupported URI:"+uri);
}
int row = mDb.update(tableName,values,selection,selectionArgs);
if(row >0){
//通知外界当前的ContentProvider 数据已经发生改变
mContext.getContentResolver().notifyChange(uri,null);
}
return row;
}
}
需要注意的是,query()
、update()
、insert()
、delete()
四大方法是存在多线程并发访问的,要做好线程同步。但是本例中使用 SQLiteDataBase 不需要做线程同步,因为只有一个 DataBase 对象,而 SQLiteDataBase 对数据库的操作就是有同步处理的。
ContentProvider 还要在 ManiFest.xml 里面进行声明:
<provider
android:authorities="com.innovator.ipcserver.provider"
android:name="com.innovator.ipcserver.ContentProvider.BookProvider"
android:permission="com.innovator.ipcserver.PROVIDER"
android:exported="true">
</provider>
-
在客户端访问服务端的 ContentProvider
我们在客户端直接使用 ContentResolver 通过指定的 Uri 来访问服务端的 ContentProvider 获取数据。
show my code
/**
* 客户端的 ContentResolver
*/
public class ProviderActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_provider);
//这个 Uri 是由 ContentProvider 声明的 AUTHORITY 参数决定的,在末尾加上要访问的表名就可以了
Uri uri = Uri.parse("content://com.innovator.ipcserver.provider/book");
//插入 book 数据
ContentValues values = new ContentValues();
values.put("_id",6);
values.put("name","程序艺术设计");
getContentResolver().insert(uri,values);
Cursor bookCursor = getContentResolver().query(uri,new String[]{"_id","name"},null,null,null);
while (bookCursor != null && bookCursor.moveToNext()){
Book book = new Book();
book.setPrice(bookCursor.getInt(0));
book.setName(bookCursor.getString(1));
Log.i("TAG","获取到的 Book:"+book.toString());
}
bookCursor.close();
Uri userUri = Uri.parse("content://com.innovator.ipcserver.provider/user");
Cursor userCursor = getContentResolver().query(userUri,new String[]{"_id","name","sex"},null,null,null);
while (userCursor != null && userCursor.moveToNext()){
User user = new User();
user.setId(userCursor.getInt(0));
user.setName(userCursor.getString(1));
user.setSex(userCursor.getInt(2));
Log.i("TAG","获取到的 User:"+user.toString());
}
userCursor .close();
}
}
客户端打印的结果:
客户端的 Log至此我们成功地使用 ContentProvider 进行了跨进程通信,关于 ContentProvider 的更多高级使用,我们以后再探讨,此处只是作为 IPC 的例子进行讲解。
2. 使用 Socket 进行 IPC
Socket 一般是用于网络通信的,支持传输任意字节流,也可以用于跨进程通信。
下面我们用 Socket 实现跨进程的聊天程序。
思路:我们在服务端建立一个 Service ,然后在这个 Service 建立一个 TCP 服务,然后在客户端的主界面中连接 TCP 服务,连接上后客户端就可以和服务端通信了。可以在服务端做能够连接多个客户端的功能。
-
建立服务端
在服务端新建一个 Service ,然后通过客户端去启动服务端的 Service,当 Service 启动的时候,会在线程中建立 ServerSocket 以提供 TCP 服务,这里监听 8688 端口,然后等待客户端连接。当有客户端连接的时候,都会生成一个新的 Socket ,通过每次新创建的 Socket 回复客户端,不同的客户端通信了。
show my code
/**
*
* Socket 服务端
* Created by innovator on 2018/2/5.
*/
public class TCPServerService extends Service {
private boolean mIsServiceDestoyed = false;
private String[] mDefinedMessages = new String[] {
"你好啊,哈哈",
"请问你叫什么名字呀?你成功引起了我的注意",
"今天真冷,什么时候才能回暖啊",
"你知道吗?我可是可以和多个人同时聊天的哦",
"给你讲个笑话吧,据说爱笑的人运气都不会太差,不知道是不是真的"
};
@Override
public void onCreate() {
Log.i("TCP","正在启动服务端的Service");
new Thread(new TcpServer()).start();
super.onCreate();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onDestroy() {
mIsServiceDestoyed = true;
super.onDestroy();
}
private class TcpServer implements Runnable{
@SuppressWarnings("resource")
@Override
public void run() {
ServerSocket serverSocket = null;
try {
//监听 8688 端口
serverSocket = new ServerSocket(8688);
}catch (IOException i){
Log.i("TCP","establish tcp server failed,port 8688,"+i.getMessage());
i.printStackTrace();
return;
}
//死循环来读客户端的消息
while (!mIsServiceDestoyed){
try{
//接收客户端请求
final Socket client = serverSocket.accept();
Log.i("TCP","accept");
//新建一个线程,因此可以和多个客户端连接
new Thread(){
@Override
public void run() {
try {
responseClient(client);
}catch (IOException o){
o.printStackTrace();
}
}
}.start();
}catch (IOException e){
e.printStackTrace();
}
}
}
}
/**
* 服务端回应客户端
* @param client
* @throws IOException
*/
private void responseClient(Socket client) throws IOException{
//接收客户端的消息
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
// 向客户端发送消息
PrintWriter out = new PrintWriter(new BufferedWriter(
new OutputStreamWriter(client.getOutputStream())),true);
out.println("欢迎来到聊天室!");
while (!mIsServiceDestoyed){
String str = in.readLine();
if (str == null){
//客户端断开连接
break;
}
Log.i("TCP","正在读取客户端发送过来的消息: "+str);
int i = new Random().nextInt(mDefinedMessages.length);
String msg = mDefinedMessages[i];
//回复客户端,进行聊天
out.println(msg);
Log.i("TCP","回复客户端: "+msg);
}
//客户端断开连接后需要关闭流
Log.i("TCP","客户端断开连接");
try {
if (null != out) {
out.close();
}
} catch (Exception e) {
e.printStackTrace();
}
try {
if (null != in) {
in.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
注意: 由于 String str = in.readLine();
是阻塞的,所以在客户端和服务端互相发送信息的时候,必须使用 out.println(msg);
,因为 println
自带换行符,所以在读取 Socket 的输入流的时候不会阻塞。
-
建立客户端
在客户端的 Activity 中启动服务端的 Service ,然后使用 Socket 监听 8688 端口,连接服务器,然后和服务器通信。
show my code
/**
* Socket 客户端
*/
public class SocketActivity extends AppCompatActivity implements View.OnClickListener{
private static final int MESSAGE_RECEIVE_NEW_MSG = 1;
private static final int MESSAGE_SOCKET_CONNECTED = 2;
private Button mSendButton;
private TextView mMessagetextView;
private TextView mMessagetEditText;
private PrintWriter mPrintWriter;
private Socket mClientSocket;
@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case MESSAGE_RECEIVE_NEW_MSG:
//显示服务器发送的消息
mMessagetextView.setText(mMessagetextView.getText()+(String)msg.obj);
break;
case MESSAGE_SOCKET_CONNECTED:
mSendButton.setEnabled(true);
break;
default:
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_socket);
mMessagetextView = findViewById(R.id.msg_container);
mSendButton = findViewById(R.id.send);
mSendButton.setOnClickListener(this);
mMessagetEditText = findViewById(R.id.msg);
Intent i = new Intent();
i.setAction("com.innovator.socket");
i.setPackage("com.innovator.ipcserver");
startService(i);
new Thread(){
@Override
public void run() {
//连接服务器
connectTCPServer();
}
}.start();
}
@Override
protected void onDestroy() {
if(mClientSocket != null){
//关闭 Socket
try {
mClientSocket.shutdownInput();
mClientSocket.close();
}catch (Exception e){
e.printStackTrace();
}
}
super.onDestroy();
}
@Override
public void onClick(View v) {
if(v == mSendButton){
String msg = mMessagetEditText.getText().toString();
if(!TextUtils.isEmpty(msg) && mPrintWriter != null){
Log.i("TCP","sending message to the server");
mPrintWriter.println(msg);
mMessagetEditText.setText("");
String time = formateDateTime(System.currentTimeMillis());
String showMsg = "self"+time+":"+msg+"\n";
//显示发送的消息
mMessagetextView.setText(mMessagetextView.getText() + showMsg);
}
}
}
private String formateDateTime(long time){
return new SimpleDateFormat("(HH:mm:ss)").format(new Date(time));
}
/**
* 连接服务端的 Socket
*/
private void connectTCPServer(){
Socket socket = null;
while (null == socket){
try{
socket = new Socket("localhost",8688);
mClientSocket = socket;
mPrintWriter = new PrintWriter(new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream())),true);
mHandler.sendEmptyMessage(MESSAGE_SOCKET_CONNECTED);
mPrintWriter.println("我是客户端");
Log.i("TCP","连接到了服务端的Socket");
}catch (IOException i){
SystemClock.sleep(1000);
i.printStackTrace();
Log.i("TCP","连接服务端的Socket失败,正在重连..."+i.getMessage());
}
}
try{
//接收服务端发送过来的信息
BufferedReader br = new BufferedReader(new InputStreamReader(
socket.getInputStream()));
while (!SocketActivity.this.isFinishing()){
String msg = br.readLine();
if(msg != null){
Log.i("TCP","服务端发送的消息:"+msg);
String time = formateDateTime(System.currentTimeMillis());
String showMsg = "server"+time+":"+msg+"\n";
//显示接收的消息
mHandler.obtainMessage(MESSAGE_RECEIVE_NEW_MSG,showMsg).sendToTarget();
}
}
Log.i("TCP","quit...");
try {
if (null != mPrintWriter) {
mPrintWriter.close();
}
} catch (Exception e) {
e.printStackTrace();
}
try {
if (null != br) {
br.close();
}
} catch (Exception e) {
e.printStackTrace();
}
socket.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
注意:当客户端销毁的时候记得释放资源,断开 Socket ,断开和服务器的连接。
-
结果:
使用 Socket 进行 IPC
3. 总结
到这里我们就已经尝过了所有的 IPC 方式,下一篇我们好好对比总结一下,下一篇再见👋👋👋
网友评论