美文网首页
Android 开发艺术探索笔记(六) 之 Android 使用

Android 开发艺术探索笔记(六) 之 Android 使用

作者: innovatorCL | 来源:发表于2018-02-08 22:37 被阅读20次

    上文 介绍过如何使用 Messenger 和 AIDL 进行跨进程通信,这一篇来讲一下如何用 ContentProvider 和 Socket 进行 IPC。

    1. 使用 ContentProvider 进行 IPC

    ContentProvider 是 Android 中专门用于不同应用间进行数据共享的方式,它的底层也是 Binder 。

    • 创建一个自定义的 ContentProvider

      我们只需要继承 ContentProvider 并且实现 onCreate(),query(),update(),insert(),delete(),getType() 方法就可以了。这里面除了 onCreate()运行在系统的主线程中,其他 5 个方法都由外界回调并运行在 Binder 线程池中。

      然后在 BookProvider 中创建一个数据库,供外界查询。

      服务端的源码:https://github.com/innovatorCL/IPCServer

    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 获取数据。

      客户端的源码:https://github.com/innovatorCL/IPCClient

    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 回复客户端,不同的客户端通信了。

      服务端的源码:https://github.com/innovatorCL/IPCServer

    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 端口,连接服务器,然后和服务器通信。

      客户端的源码:https://github.com/innovatorCL/IPCClient

    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 方式,下一篇我们好好对比总结一下,下一篇再见👋👋👋

    相关文章

      网友评论

          本文标题:Android 开发艺术探索笔记(六) 之 Android 使用

          本文链接:https://www.haomeiwen.com/subject/xeuxtftx.html