Android艺术开发探索——第二章(IPC机制(下))

仓廪实则知礼节,衣食足则知荣辱。这篇文章主要讲述Android艺术开发探索——第二章:IPC机制(下)相关的知识,希望能为你提供帮助。
android艺术开发探索——第二章: IPC机制( 下)

我们继续来讲IPC机制, 在本篇中你将会学习到
  • ContentProvider
  • Socket
  • Binder连接池
一.使用ContentProvider
ContentProvider是Android中提供的专门用来不同应用之间数据共享的方式, 从这一点来看, 他天生就是适合进程间通信, 和Messenger一样, ContentProvider的底层实现同样也是Binder, 由此可见, Binder在Android系统中是何等的重要, 虽然ContentProvider的底层实现是Binder, 但是他的使用过程比AIDL简单多了, 这是因为系统为我们封装了, 使得我们无须关心底层实现即可轻松实现IPC, ContentProvider虽然使用起来很简单, 包括自己创建一个ContentProvider也不是什么难事, 尽管如此, 它的细节还是相当多, 比如CRUD操作, 防止SQL注入和权限控制等。由于章节主题限制, 在本节中, 笔者暂时不对ContentProvider的使用细节以及工作机制进行详细分析, 而是为读者介绍采用ContentProvider进行跨进程通信的主要流程, 至于使用细节和内部工作机制会在后续章节进行详细分析。
系统预置了许多ContentProvider, 比如通讯录信息、日程表信息等, 要跨进程访问这些信息, 只需要通过ContentResolver的query,update、insert 和 delete方法即可, 在本节中, 我们来实现一个自定义的ContentProvider, 并演示如何在其他应用中获取ContentProvider中的数据从而实现进程间通信这一目的。首先, 我们创建一个ContentProvider名字就叫BookProvider。创建一个自定义的ContentProvider很简单, 只需要继承ContentProvider并且实现它的六个方法: onCreate、query、update、 insert和getType,这六个抽象方法都很好理解, onCreate代表ContentProvider的创建, 一般我们要做一些初始化工作; getIype用来返回一个Uri请求的MIME类型( 媒体类型, 比如图片) , 这个媒体类型还是比较复杂的, 如果我们的应用不关注这些选项, 可以直接在这个方法中返回null或者/, 剩下的四个方法对应于CRUD操作, 即实现对数据表的增删查改功能, 除了Binder的工作原理, 我们知道这六个方法均运行在ContentProvider的进程中, 除了onCreate由系统回调并并运行在主线程中, 其他五个方法均由外界回调并且运行在Binder线程池中, 这一点我们再接下来的例子中可以看到。
ContentProvider主要以表格的形式来组织数据, 并且可以包含多个表, 对于每个表格来说, 它们都具有行和列的层次性, 行往往对应一条记录, 而列对应一条记录中的一个字段, 这点和数据库很类似。除了表格的形式, ContentProvider还支持文件数据, 比如图片、视频等。文件数据和表格数据的结构不同, 因此处理这类数据时可以在ContentProvider中返回文件的句柄给外界从而让文件来访问Contentprovider中的文件信息。Android系统所提供的MediaStore功能就是文件类型的ContentProvider, 详细实现可以参考MediaStore。另外, 虽然ContentProvide的底层数据看起来很像一个SQLite数据库, 但是ContentProvider对底层的数据存储方式没有任何要求, 我们既可以使用SQLite数据库, 也可以使用普通的文件, 甚至可以采用内存中的一个对象来进行数据的存储, 这一点在后续的章节中会再次介绍, 所以这里不再深入了。
下面看一个最简单的示例, 它演示了ContentProvider的工作工程。首先创建一个BookProvider类, 它继承自ContentProvider并实现了ContentProvider的六个必须需要实现的抽象方法。在下面的代码中, 我们什么都没干, 尽管如此, 这个BookProvider也是可以
工作的, 只是它无法向外界提供有效的数据而已。
package com.liuguilin.contentprovidersampler; /* *项目名: ContentProviderSampler *包名: com.liuguilin.contentprovidersampler *文件名:BookProvider *创建者:LGL *创建时间:2016/10/20 13:49 *描述: ContentProvider */import android.content.ContentProvider; import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; import android.support.annotation.Nullable; import android.util.Log; public class BookProvider extends ContentProvider{public static final String TAG = " BookProvider" ; @ Override public boolean onCreate() { Log.i(TAG," onCreate,current thread:" + Thread.currentThread().getName()); return false; }@ Nullable @ Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { Log.i(TAG," query,current thread:" + Thread.currentThread().getName()); return null; }@ Nullable @ Override public String getType(Uri uri) { Log.i(TAG," getType" ); return null; }@ Nullable @ Override public Uri insert(Uri uri, ContentValues values) { Log.i(TAG," insert" ); return null; }@ Override public int delete(Uri uri, String selection, String[] selectionArgs) { Log.i(TAG," delete" ); return 0; }@ Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { Log.i(TAG," update" ); return 0; } }

接着我们需要注册这个BookProvider, 如下所示。其中android:authorities是ContenttProvider的唯一标识, 通过这个属性外部应用就可以访问我们的BookProvider, 因此android:authorities必须是唯一的, 这里建议读者在命名的时候加上包名前缀。,为了演示进程间通讯, 我们让BookProvider运行在独立的进程中并给它添加了权限, 这样外界应用如果想访问BookProvider, 就必须声明com.lgl.PROVIDER这个权限。ContentProvider的的权限还可以细分为读权限和写权限, 分别对应androidreadPermission和
androidswritePermission 属性, 如果分别声明了读权限和写权限, 那么外界应用也必须依次声明相应的权限才可以进行读/写操作, 否则外界应用会异常终止。关于权限这一块, 请读者自行查阅相关资料, 本章不进行详细介绍。
< provider android:name= " .BookProvider" android:authorities= " com.liuguilin.contentprovidersampler.BookProvider" android:permission= " com.lgl.PROVIDER" android:process= " :provider" />

注册了ContentProvider之后, 我们就可以在外部应用中访问他了, 为了方便演示, 这里我们再统一个应用中其他进程去访问这个BookProvider,和其他应用中的访问效果一样, 读者可以自行试下( 要声明权限)
package com.liuguilin.contentprovidersampler; /* *项目名: ContentProviderSampler *包名: com.liuguilin.contentprovidersampler *文件名:ProviderActivity *创建者:LGL *创建时间:2016/10/20 13:55 *描述: ContentProvider类 */import android.net.Uri; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; public class ProviderActivity extends AppCompatActivity{@ Override protected void onCreate( Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_provider); Uri uri = Uri.parse(" content//com.liuguilin.contentprovidersampler.BookProvider" ); getContentResolver().query(uri,null,null,null,null); getContentResolver().query(uri,null,null,null,null); getContentResolver().query(uri,null,null,null,null); } }

在上面的代码中, 我们通过ContentResolver对象的query方法去查询BookProvider中的数据, 其中“content//com.liuguilin.contentprovidersampler.BookProvider”唯一标识了BookProvider, 而这
个标识正是我们前面为BookProvider的android:authorities属性所指定的值。我们运行后看一下 log。从下面log可以看出, BookProvider中的query方法被调用了三次, 并且这三次调用不在同一个线程中。可以看出, 它们运行在一个Binder线程中, 前面提到update、insert和delete方法同样也运行在Binder线程中。另外, onCreate运行在main线程中, 也就是
UI线程, 所以我们不能在onCreate中做耗时操作。
到这里, 整个ContentProvider的流程我们已经跑通了, 虽然ContentProvider中没有返回任何数据。接下来, 在上面的基础上, 我们继续完善BookProvider, 从而使其能够对外应用提供数据, 继续本章提出的那个例子, 现在我们要提供一个BookProvider, 外部应用可以通过BookProvider来访问图书信息, 为了更好地演示ContentProvider, 还可以通过BookProvider访问到用户信息。为了完成上述功能, 我们需要一个数据库来来管理图书和用户信息, 这个数据库不难实现, 代码如下,
package com.liuguilin.contentprovidersampler; /* *项目名: ContentProviderSampler *包名: com.liuguilin.contentprovidersampler *文件名:DbOPenHelper *创建者:LGL *创建时间:2016/10/20 13:58 *描述: 数据库 */import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; public class DbOPenHelper extends SQLiteOpenHelper {public static final String DB_NAME = " book_provider.db" ; public static final String BOOK_TABLE_NAME = " book" ; public static final String USER_TABLE_NAME = " user" ; public static final int DB_VERSION = 1; //图书和用户信息表 private String CREATE_BOOK_TABLE = " CREATE TABLE ID NOT EXISTS" + BOOK_TABLE_NAME + " (_id INTEGER PRIMARY KEY," + " name TEXT)" ; private String CREATE_USER_TABLE = " CREATE TABLE IF NOT EXISTS" + USER_TABLE_NAME + " (_id INTEGER PRIMARY KEY," + " name TEXT," ; public DbOPenHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); }@ Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE_BOOK_TABLE); db.execSQL(CREATE_USER_TABLE); }@ Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {} }

上述代码是一个最简单的数据库的实现, 我们借助SQLiteOpenHelper来管理数据库的创建、升级和降级。下面我们就要通过BookProvider向外界提供上述数据库中的信息了,我们知道, ContentProvider通过Uri来区分外界要访问的的数据集合, 在本例中支持
对BookProvider中的book表和user表进行访问, 为了知道外界要访问的是哪个表, 我需要为它们定义单独的Uri和Uri_Code, 并将Uri和对应的Uru_Code相关联, 我们可以用UriMatcher的addURI方法将Uri和Ur_Code关联到一起。这样, 当外界请求访问BookProvider时, 我们就可以根据请求的Uri来得到Ur_Code, 有了Uri_Code我们就知道外界想要访问哪个表, 然后就可以进行相应的数据操作了, 具体代码如下
public class BookProvider extends ContentProvider {public static final String TAG = " BookProvider" ; public static final String AUTHORITY = " com.liuguilin.contentprovidersampler.BookProvider" ; 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; public 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先取出Uri_code, 关联的都是0和1, 这个关联过程就是一句话
sUriMatcher.addURI(AUTHORITY," book" ,BOOK_URI_CODE); sUriMatcher.addURI(AUTHORITY," user" ,USER_URI_CODE);

将Uri和uri_code管理好之后, 我们可以通过如下方式来获取外界需要访问的数据, 根据Uri先取出uri_code, 根据Uri_code再来得到表的名称, 接下来我么可以响应外界的增删查改请求了
private String getTableName(Uri uri) { String tableName = null; switch (sUriMatcher.match(uri)) { case BOOK_URI_CODE: tableName = DbOPenHelper.BOOK_TABLE_NAME; break; case USER_URI_CODE: tableName = DbOPenHelper.USER_TABLE_NAME; break; } return tableName; }

接着我们就可以实现增删查改的方法了, 如果是qurey, 首先我们要从拿到外界要访问的表名称, 然后根据外界传递的信息进行数据库的查询操作了, 这个过程比较简单:
@ Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { Log.i(TAG, " query,current thread:" + Thread.currentThread().getName()); String table = getTableName(uri); if(table = = null){ throw new IllegalArgumentException(" Unsupported URI:" + uri); } return mDb.query(table,projection,selection,selectionArgs,null,null,sortOrder,null); }

另外三个方法的实现思路和查询有点类似, 只有一点不同, 那就是这三个方法都会引起数据源的改变, 这个时候我们需要通过ContentResolver的notifyChange中的数据改变情况, 可以通过注册的方法来注册观察者, 对于这三个方法, 这里不再详细说, 看代码:
package com.liuguilin.contentprovidersampler; /* *项目名: ContentProviderSampler *包名: com.liuguilin.contentprovidersampler *文件名:BookProvider *创建者:LGL *创建时间:2016/10/20 13:49 *描述: ContentProvider */import android.content.ContentProvider; import android.content.ContentValues; import android.content.Context; import android.content.UriMatcher; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.support.annotation.Nullable; import android.util.Log; public class BookProvider extends ContentProvider {public static final String TAG = " BookProvider" ; public static final String AUTHORITY = " com.liuguilin.contentprovidersampler.BookProvider" ; 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; public static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); static { sUriMatcher.addURI(AUTHORITY, " book" , BOOK_URI_CODE); sUriMatcher.addURI(AUTHORITY, " user" , USER_URI_CODE); }private Context mContext; private SQLiteDatabase mDb; @ Override public boolean onCreate() { Log.i(TAG, " onCreate,current thread:" + Thread.currentThread().getName()); mContext = getContext(); initProviderDate(); return true; }private void initProviderDate() { 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,' html5' ); " ); mDb.execSQL(" insert into book values(1,' jake' ,1); " ); mDb.execSQL(" insert into book values(2,' Jasmine' ,0); " ); }@ Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { Log.i(TAG, " query,current thread:" + Thread.currentThread().getName()); String table = getTableName(uri); if (table = = null) { throw new IllegalArgumentException(" Unsupported URI:" + uri); } return mDb.query(table, projection, selection, selectionArgs, null, null, sortOrder, null); }@ Nullable @ Override public String getType(Uri uri) { Log.i(TAG, " getType" ); return null; }@ Nullable @ Override public Uri insert(Uri uri, ContentValues values) { Log.i(TAG, " insert" ); String table = getTableName(uri); if (table = = null) { throw new IllegalArgumentException(" Unsupported URI:" + uri); } mDb.insert(table, null, values); mContext.getContentResolver().notifyChange(uri, null); return uri; }@ Override public int delete(Uri uri, String selection, String[] selectionArgs) { Log.i(TAG, " delete" ); String table = getTableName(uri); if (table = = null) { throw new IllegalArgumentException(" Unsupported URI:" + uri); } int count = mDb.delete(table, selection, selectionArgs); if (count > 0) { getContext().getContentResolver().notifyChange(uri, null); } return count; }@ Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { Log.i(TAG, " update" ); String table = getTableName(uri); if (table = = null) { throw new IllegalArgumentException(" Unsupported URI:" + uri); } int row = mDb.update(table, values, selection, selectionArgs); if (row > 0) { getContext().getContentResolver().notifyChange(USER_CONTENT_URI, null); } return row; }private String getTableName(Uri uri) { String tableName = null; switch (sUriMatcher.match(uri)) { case BOOK_URI_CODE: tableName = DbOPenHelper.BOOK_TABLE_NAME; break; case USER_URI_CODE: tableName = DbOPenHelper.USER_TABLE_NAME; break; } return tableName; } }

需要注意的是, 增删查改四大方法是存在多线程并发访问的, 因此方法内部要做好线程同步的工作, 在本例中, 由于采取了sqlite并且只有一个SQLiteDatabase内部对数据库的操作式同步处理的, 但是如果多个SQLiteDatabase对象来操作数据库就无法保证线程同步了, 因为SQLiteDatabase对象之间无法进程线程同步, 如果ContentProvider的底层数据集是一块内存的话, 比如List, 在这种情况下同List的遍历, 插入, 删除操作就需要进行线程同步了, 否则会引发错误, 这点尤其需要注意的, 到这里BookProvider已经完成了, 接着我们来外部访问他, 看看他能否继续工作
package com.liuguilin.contentprovidersampler; /* *项目名: ContentProviderSampler *包名: com.liuguilin.contentprovidersampler *文件名:ProviderActivity *创建者:LGL *创建时间:2016/10/20 13:55 *描述: ContentProvider类 */import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; public class ProviderActivity extends AppCompatActivity{public static final String TAG = " ProviderActivity" ; @ Override protected void onCreate( Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_provider); Uri bookUri = Uri.parse(" content//com.liuguilin.contentprovidersampler.BookProvider/book" ); ContentValues values = new ContentValues(); values.put(" _id" ,6); values.put(" name" ," 程序设计的艺术" ); getContentResolver().insert(bookUri,values); Cursor bookCursor = getContentResolver().query(bookUri,new String[]{" _id" ," name" },null,null,null); while (bookCursor.moveToNext()){ Book book = new Book(); book.id = bookCursor.getInt(0); book.name = bookCursor.getString(1); } bookCursor.close(); Uri userUri = Uri.parse(" content//com.liuguilin.contentprovidersampler.BookProvider/user" ); Cursor userCursor = getContentResolver().query(userUri,new String[]{" _id" ," name" ," sex" },null,null,null); while (userCursor.moveToNext()){ User user = new User(); user.id= userCursor.getInt(0); user.name = userCursor.getString(1); user.isMale = userCursor.getInt(2) = = 1; } userCursor.close(); } }

默认情况下, BookProvider的数据库中有三本书和两个用户, 在上面的代码中, 我们首先添加一本书: “程序设计的艺术”。接着查询所有的图书, 这个时候应该查询出四本书, , 因为我们刚刚添加了一本。然后查询所有的用户, 这个时候应该查询出两个用户。是不是这样呢?我们运行一下程序, 从log可以看到, 我们的确查询到了4本书和2个用户, 这说明BookProvider已经能够正确地处理外部的请求了, 读者可以自行验证一下update和delete操作, 这里就不再验证了。同时, 由于ProviderActivity和BookProvider运行在两个不同的进程中, 因此, 这也构成了进程间的通信。ContentProvider除了支持对数据源的增删改查这四个操作, 还支持自定义调用, 这个过程是通ContentResolver的Call方法和ContentProvider的Call方法来完成的。关于使用ContentProvider来进行IPC就介绍到这里, ContentProvider本身还有一些细节这里并没有介绍, 读者可以自行了解, 本章侧重的是各种进程间通信的方法以及它们的区别, 因此针对某种特定的方法可能不会介绍得面面俱到。另外, ContentProvider在后续章节还会有进一步的讲解, 主要包括细节问题和工作原理, 读者可以阅读后面的相应章节
二.使用Socket
在本节, 我们通过Socket来实现进程通信, Socket也叫做套接字, 是网络通信中的概念, 他分为流式套接字和用户数据报套接字两种, 分别是应用于网络的传输控制层中的Tcp和UDP协议, TCP面向的连接协议, 提供稳定的双向通讯功能, TCP连接的建立需要经过“三次握手”才能完成, 为了提供稳定的数据传输功能, 其本身提供了超时重传机制, 因此具有很高的稳定性: 而UDP是无连接的, 提供不稳定的单向通信功能, 当然UDP也可以实现双向通信功能。在性能上, UDP具有更好的效率, 其缺点是不保证数据一定能够正确传输, 尤其是在网络拥塞的情况下。关于TCP和UDP的介绍就这么多, 更详细的资料请查看相关网络资料。接下来我们演示一个跨进程的聊天程序, 两个进程可以通过Socket来实现信息的传输, Socket本身可以支持传输任意字节流, 这里为了简单起见, 仅仅传输文本信息, 很显然, 这是一种IPC方式。
使用Socket来进行通信, 有两点需要注意, 首先需要声明权限:
< uses-permission android:name= " android.permission.INTERNET" /> < uses-permission android:name= " android.permission.ACCESS_NETWORK_STATE" />

其次要注意不能在主线程中访问网络, 因为这会导致我们的程序无法在Android4.0及其以上的设备中运行, 会抛出如下异常: android.os NetworkOnMainThreadException。而且进行网络操作很可能是耗时的, 如果放在主线程中会影响程序的响应效率, 从这方面来说, 也不应该在主线程中访问网络。下面就开始设计我们的聊天室程序了, 比较简单, 首先在远程Service建立一个TCP服务, 然后在主界面中连接TCP服务, 连接上了以后, 就可给服务端发消息。对于我们发送的每一条文本消息, 服务端都会随机地回应我们一句话为了更好地展示Socket的工作机制, 在服务端我们做了处理, 使其能够和多个客户端同时连接建立连接并响应。
先看一下服务端的设计, 当Service启动时, 会在线程中建立TCP服务, 这里监听的是8688端口, 然后就可以等待客户端的连接请求。当有客户端连接时, 就会生成一个新的Socket, 通过每次新创建的Socket就可以分别和不同的客户端通信了。服务端每收到一次客户端的消息就会随机回复一句话给客户端。当客户端断开连接时, 服务端这边也会相应的关闭对应Socket并结束通话线程, 这点是如何做到的呢?方法有很多, 这里是通过判断服务端输入流的返回值来确定的, 当客户端断开连接后, 服务端这边的输入流会返回null,这个时候我们就知道客户端退出了。服务端的代码如下所示。
package com.liuguilin.contentprovidersampler; /* *项目名: ContentProviderSampler *包名: com.liuguilin.contentprovidersampler *文件名:TCPServerService *创建者:LGL *创建时间:2016/10/22 15:16 *描述: 服务端 */import android.app.Service; import android.content.Intent; import android.os.IBinder; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.Random; public class TCPServerService extends Service {private boolean mIsServiceDestoeyed = false; private String[] mDefinedMessages = {" 你好呀" , " 你叫神马? " , " 今天的天气" , " 汽车站怎么走? " }; @ Override public void onCreate() { new Thread(new TcpServer()).start(); super.onCreate(); }@ Override public IBinder onBind(Intent intent) { return null; }@ Override public void onDestroy() { mIsServiceDestoeyed = true; super.onDestroy(); }private class TcpServer implements Runnable{@ Override public void run() { ServerSocket serverSocket = null; try { //监听本地8868端口号 serverSocket = new ServerSocket(8688); } catch (IOException e) { e.printStackTrace(); return; }while (!mIsServiceDestoeyed){ try { //接收客户端的请求 final Socket client = serverSocket.accept(); new Thread(){ @ Override public void run() { try { responseClient(client); } catch (IOException e) { e.printStackTrace(); } } }.start(); } catch (IOException e) { e.printStackTrace(); } } } }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.print(" 欢迎来到聊天室" ); while (!mIsServiceDestoeyed){ String str = in.readLine(); if(str = = null){ break; } int i = new Random().nextInt(mDefinedMessages.length); String msg = mDefinedMessages[i]; out.print(msg); } in.close(); out.close(); client.close(); } }

接下来看一下客户端, 客户端Activity启动时, 会在onCreate中开启一个线程去连接服务端的socket, 至于为什么要用线程我们前面已经说了, 为了确定能够连接成功, 这里采用了超时重连的机制, 每次连接失败后都会重新连接, 当然, 为了降低重试机制的开销, 我们加入了休眠机制, 每次重试的事件间隔为1000毫秒
try { //接收服务端的消息 BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); while (!TCPClientActivity.this.isFinishing()) { String msg = br.readLine(); if (msg != null) { String time = formatDateTime(System.currentTimeMillis()); String showMsg = " server " + time + " : " + msg + " \\n" ; handler.obtainMessage(MESSAGE_RECEIVE_NEW_MSG, showMsg).sendToTarget(); } }

当然, 你的activity在退出的时候, 就要关闭socket了
@ Override protected void onDestroy() { if (mClientSocket != null) { try { mClientSocket.shutdownInput(); mClientSocket.close(); } catch (IOException e) { e.printStackTrace(); } } super.onDestroy(); }

接收发送消息的整个过程, 这个就很简单了, 看完整代码:
package com.liuguilin.contentprovidersampler; /* *项目名: ContentProviderSampler *包名: com.liuguilin.contentprovidersampler *文件名:TCPClientActivity *创建者:LGL *创建时间:2016/10/22 15:31 *描述: 客户端 */import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.support.v7.app.AppCompatActivity; import android.text.TextUtils; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.net.Socket; import java.text.SimpleDateFormat; import java.util.Date; public class TCPClientActivity extends AppCompatActivity implements View.OnClickListener {public static final int MESSAGE_RECEIVE_NEW_MSG = 1; public static final int MESSAGE_SOCKET_CONNECTED = 2; private Button mSendButton; private TextView mMessageTextView; private EditText mMessageEditText; private PrintWriter mPrintWriter; private Socket mClientSocket; private Handler handler = 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; } } }; @ Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_tcp_client); initView(); }private void initView() { mMessageTextView = (TextView) findViewById(R.id.msg_container); mSendButton = (Button) findViewById(R.id.send); mSendButton.setOnClickListener(this); mMessageEditText = (EditText) findViewById(R.id.msg); Intent intent = new Intent(this, TCPServerService.class); startService(intent); new Thread() { @ Override public void run() { connectTCPServer(); } }.start(); }@ Override protected void onDestroy() { if (mClientSocket != null) { try { mClientSocket.shutdownInput(); mClientSocket.close(); } catch (IOException e) { e.printStackTrace(); } } super.onDestroy(); }@ Override public void onClick(View v) { if (v = = mSendButton) { String msg = mMessageEditText.getText().toString(); if (!TextUtils.isEmpty(msg)) { mPrintWriter.println(msg); mMessageEditText.setText(" " ); String time = formatDateTime(System.currentTimeMillis()); String showesMsg = " self" + time + " :" + msg + " \\n" ; mMessageTextView.setText(mMessageTextView.getText() + showesMsg); } } }private String formatDateTime(long l) { return new SimpleDateFormat(" (HH:mm:ss)" ).format(new Date(l)); }private void connectTCPServer() { Socket socket = null; while (socket = = null) { try { socket = new Socket(" localhost" , 8688); mClientSocket = socket; mPrintWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true); handler.sendEmptyMessage(MESSAGE_SOCKET_CONNECTED); } catch (IOException e) { e.printStackTrace(); } } try { //接收服务端的消息 BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); while (!TCPClientActivity.this.isFinishing()) { String msg = br.readLine(); if (msg != null) { String time = formatDateTime(System.currentTimeMillis()); String showMsg = " server " + time + " : " + msg + " \\n" ; handler.obtainMessage(MESSAGE_RECEIVE_NEW_MSG, showMsg).sendToTarget(); } }mPrintWriter.close(); br.close(); socket.close(); } catch (IOException e) { e.printStackTrace(); } }}

上述代码就是通过Socket来进行进程间的通信实例, 除了采用套接字, 还可以用UDP套接字, OK
三.Binder连接池
上面我们介绍了不同的IPC方式, 我们知道, 不同的IPC方式有不同的特点和适用场景,当然这个问题会在26节进行介绍, 在本节中要再次介绍一下AIdL, 原因是AIDL是一种最常用的进程间通信方式, 是日常开发中涉及进程间通信时的首选, 所以我们需要额外强调一下.
如何使用AIDL在上面的一节中已经进行了介绍, 这里在回顾一下大致流程: 首先创建一个Service和一个AIDL接口, 接着创建一个类继承自AIDL接口中的Stub类并实现Stub中的抽象方法, 在Service的onBind方法中返回这个类的对象, 然后客户端就可以绑
定服务端Service, 建立连接后就可以访问远程服务端的方法了。上述过程就是典型的AIDL的使用流程。这本来也没什么问题, 但是现在考虑一种情况; 公司的项目越来越庞大了, 现在有10个不同的业务模块都需要使用AIDL来进行进程间通信, 那我们该怎么处理呢? 也许你说: “就按照AIDL的实现方式一个个来吧”, 这是可以的, 如果用这种方法, 首先我们需要创建10个Service, 这好像有点多啊! 如果有100个地方需要用到AIDL呢, 先创建100个Servlce?到这里, 读者应该明白问题所在了, 随着AIDL数量的增加, 我们不能无限制地增Service,Service是四大组件之一, 本生是一种系统资源。而且太多的Serice会使得我们的应用看起来很重量级, 因为正在运行的Service可以在应用详情页看到, 当我们的应用详情显示有10个个服务正在运行时, 这看起来并不是什么好事。针对上述问题, 我们需要减少Service的数量, 将所有的AIDL放在同个Service中去管理。
【Android艺术开发探索——第二章(IPC机制(下))】在这种模式下, 整个工作机制是这样的; 每个业务模块创建自己的AIDL接口并实现此接口, 这个时候不同业务模块之间是不能有耦合的, 所有实现细节我们要单独开来, 然后向服务端提供自己的唯一标识和其对应的Binder对象; 对于服务端来说, 只需要一个Service就可以了, 服务端提供一个queryBinder接口, 这个接口能够根据业务模块的特征来返回相应的Binder对象给它们, 不同的业务模块拿到所需的Binder对象后就可以进行远程方法调用了。由此可见, Binder连接池的主要作用就是将每个业务模块的Binder请求统一转发到远程Service中去执行, 从而避免了重复创建Service的过程, 它的工作原理如图所示。
Android艺术开发探索——第二章(IPC机制(下))

文章图片

通过上面的理论介绍, 也许还有点不好理解, 下面对Binder连接池的代码实现做一说明。首先, 为了说明问题, 我们提供了两个AIDL接口( ISecurityCenter和ICompute) 来模拟上面提到的多个业务模块都要使用AIDL的情况, 其中ISecurityCenter接口提供解密功能, 声明如下:
interface ISecurityCenter { String encrypt(String content); String decrypt(String password); }


    推荐阅读