Android进程间的通信

来源:互联网 发布:淘宝店铺的钱在哪里看 编辑:程序博客网 时间:2024/05/19 05:01

1. 多进程使用场景

1) 应用某些模块因为特殊需求需要运行在单独进程中。如消息推送,使消息推送进程与应用进程能单独存活,消息推送进程不会因为应用程序进程crash而受影响。
2) 为加大一个应用可使用的内存,需要多进程来获取多份内存空间。

2. 如何开启多进程

给四大组件(Activity、Service、Receiver、ContentProvider)在AndroidMainfest中指定Android:process属性指定。

  • 如果进程以”:”开头的进程,代表应用的私有进程,其他应用的组件不可以和它跑在同一个进程中;
  • 进程不以”:”开头的进程属于全局进程,其他应用可通过shareUID可以和它跑在同一个进程中。
  • 若两个不同应用设置了相同的shareUID,它们之间想共享数据,还需要有相同的签名才可以。

3. 多进程会造成的问题

(1) 静态成员和单例失效,数据同步失败;
Android会为每一个应用/每一个进程分配一个独立的虚拟机,不同虚拟机在内存分配上有不同的地址空间,这就导致不同虚拟机中访问同一个类对象会产生多个副本。
(2) 线程同步机制失效;
因为不同进程不是同一块内存,不同进程锁的不是同一对象。
(3) SharedPreferences可靠性下降;
SharedPreferences底层是通过读/写XML文件实现的,并发写可能会出问题,所以它不支持多个进程同时去执行写操作,否则会导致一定几率的数据丢失。
(4) Application会创建多次;
当一个组件跑在一个新进程中,系统会给它重新分配独立虚拟机,这其实就是启动一个应用的过程,故运行在不同进程中的组件属于不同的虚拟机和不同的Application。

4. 数据序列化

Intent和Binder传输数据时,或是对象持久化转存/通过网络传输给其他客户端时,需要使用Parcelable或Serializable将对象转换成可以传输的形式。
(1) Serializable接口
Serializable是Java提供的一个序列化接口,它是一个空接口,想要某个类实现序列化,只需要相应类实现Serializable接口即可。Serializable是借助ObjectOutputStream和ObjectInputStream实现对象的序列化和反序列化
一般在相应类实现Serializable类中还会定义一个final long型的serialVersionUID,不用它也能实现对象的序列化,它是用来辅助反序列化的。序列化时会把当前类的serialVersionUID写入序列化文件中,当反序列化系统会检测文件中的serialVersionUID,看它是否和当前类的serialVersionUID一致,如果一致说明序列化的类的版本和当前类的版本相同,这时可反序化成功;否则说明当前类和序列化的类相比发生了变换,如增加或减少某个成员变量,这时无法正常反序列化。
(2) Parcelable接口
在序列化过程中需要实现的功能有:
1) 序列化:由writeToParcel方法完成,最终通过Parcel中的一系列的write方法完成;
2) 反序列化:由CREATOR完成,内部标识了如何创建序列化对象和数组,最终通过Parcel的一系列read方法来完成反序列化;
3) 内容描述符:由describeContents方法来完成,几乎所有情况下该方法都返回0,仅当当前对象中存在文件描述符时返回1。
(3)两者区别
Serializable是Java中序列化接口,使用简单但开销大,序列和反序列化需要大量I/O操作;
Parcelable是Android中特有的序列化接口,使用复杂但开销小,效率高,是Android推荐的序列化方法;
Parcelable主要用在内存序列化上,如果将对象序列化到存储设备或将对象序列化后通过网络传输,过程会比较复杂,建议使用Serializable。

5. Binder

Binder是什么?

  • Binder是Android中的一个类,它继承了IBinder接口;
  • 从IPC角度来说,Binder是Android中一种跨进程通信的方式;
  • Binder还可以理解为一种虚拟的物理设备,它的设备驱动是/dev/binder;
  • 从AndroidFramework角度来说,Binder是ServiceManager连接各种Manager(ActivityManager, WindowManager)和相应ManagerService的桥梁;
  • 从Android应用层来说,Binder是客户端和服务端进行通信的媒介,当bindService时,服务端会返回一个包含了服务端业务的Binder对象,通过这个Binder对象,客户端就可以获取服务端提供的服务或数据,这里的服务包括普通服务和基于AIDL的服务。

6.android中跨进程通信方式

6.1 结合Bundle使用Intent

在一个进程中启动另一个进程的Activity, Service, Receiver组件时,可以使用Bundle附加上需要传递的消息给远程进程,并通过Intent发送出去。

6.2 使用文件共享

两个进程通过读/写同一个文件来交换数据,还可以序列化一个对象到文件系统中,从另一个进程中恢复这个对象。它的实现原理是通过ObjectOutputStream将文件写入文件中,再通过ObjectInputSteam将文件恢复。通过文件共享的方式有一定局限性,如并发读/写,读出的内容可能不是最新的,并发写就可能导致数据混乱。因此尽量避免并发写这种操作,或考虑用线程同步来限制多个线程的写操作。文件共享适合在对数据要求不高的进程之间通信。
SharedPreferences是Android提供的一种轻量级存储方案,它通过键值对方式存储数据,底层它是采用XML来存储键值对。由于系统对SharedPreferences的读写有一定的缓存策略,即在内存中会有一份SharedPreferences文件的缓存,在多进程模式下,系统对它的读/写就变得不可靠,当面对高并发读/写访问时,SharedPreferences会有很大几率会丢失数据。因此不建议在进程间通信中使用SharedPreferences。

6.3 使用Messenger

Messenager可以在不同进程中传递Message对象,在Message中放入我们要传递的数据。它的底层实现是AIDL,下面是Messenger的两个构造方法:


public Messenger(Handler target) {        mTarget = target.getIMessenger();    }public Messenger(IBinder target) {        mTarget = IMessenger.Stub.asInterface(target);    }

不管是IMessenger还是Stub.asInterface,这种使用方法都表明它的底层是AIDL,它一次只处理一个请求,不存在线程同步问题。
实现步骤:
(1) 服务端进程
1) 定义一个Service用于客户端的绑定,建立一个Handler,在handleMessage里处理客户端发送过来的消息。

//用ServiceHandler接收并处理来自于客户端的消息    private class ServiceHandler extends Handler {        @Override        public void handleMessage(Message msg) {            if(msg.what == RECEIVE_MESSAGE_CODE){                Bundle data = msg.getData();                if(data != null){                    String str = data.getString("msg");                }                //通过Message的replyTo获取到客户端自身的Messenger,                //Service可以通过它向客户端发送消息                clientMessenger = msg.replyTo;                if(clientMessenger != null){                    Message msgToClient = Message.obtain();                    msgToClient.what = SEND_MESSAGE_CODE;                    //可以通过Bundle发送跨进程的信息                    Bundle bundle = new Bundle();                    bundle.putString("msg", "你好,客户端,我是MyService");                    msgToClient.setData(bundle);                    try{                        clientMessenger.send(msgToClient);                    }catch (RemoteException e){                        e.printStackTrace();                        Log.e("DemoLog", "向客户端发送信息失败: " + e.getMessage());                    }                }            }        }    }

2) 通过Handler创建一个Messenger对象

//serviceMessenger是Service自身的Messenger,其内部指向了ServiceHandler的实例    //客户端可以通过IBinder构建Service端的Messenger,从而向Service发送消息,    //并由ServiceHandler接收并处理来自于客户端的消息    private Messenger serviceMessenger = new Messenger(new ServiceHandler());
3) 在Service的onBind中通过Messenger.getBinder()返回底层的Binder对象。

@Override    public IBinder onBind(Intent intent) {        Log.i("DemoLog", "MyServivce -> onBind");        //获取Service自身Messenger所对应的IBinder,并将其发送共享给所有客户端        return serviceMessenger.getBinder();    }
4) 注册服务,让其运行在单独进程中

<service    android:name=".MyService"    android:enabled="true"    android:process=":remote" ></service>
(2) 客户端进程
1) 绑定服务端的Service

private ServiceConnection conn = new ServiceConnection() {        @Override        public void onServiceConnected(ComponentName name, IBinder binder) {            //客户端与Service建立连接            Log.i("DemoLog", "客户端 onServiceConnected");            //我们可以通过从Service的onBind方法中返回的IBinder初始化一个指向Service端的Messenger            serviceMessenger = new Messenger(binder);            isBound = true;            Message msg = Message.obtain();            msg.what = SEND_MESSAGE_CODE;            //此处跨进程Message通信不能将msg.obj设置为non-Parcelable的对象,应该使用Bundle            //msg.obj = "你好,MyService,我是客户端";            Bundle data = new Bundle();            data.putString("msg", "你好,MyService,我是客户端");            msg.setData(data);            //需要将Message的replyTo设置为客户端的clientMessenger,            //以便Service可以通过它向客户端发送消息            msg.replyTo = clientMessenger;            try {                Log.i("DemoLog", "客户端向service发送信息");                serviceMessenger.send(msg);            } catch (RemoteException e) {                e.printStackTrace();                Log.i("DemoLog", "客户端向service发送消息失败: " + e.getMessage());            }        }        @Override        public void onServiceDisconnected(ComponentName name) {            //客户端与Service失去连接            serviceMessenger = null;            isBound = false;            Log.i("DemoLog", "客户端 onServiceDisconnected");        }    };    Intent intent = new Intent();                ComponentName componentName = new ComponentName(packageName, serviceNmae);    intent.setComponent(componentName);    try{        Log.i("DemoLog", "客户端调用bindService方法");        bindService(intent, conn, BIND_AUTO_CREATE);    }catch(Exception e){        e.printStackTrace();        Log.e("DemoLog", e.getMessage());    }}

绑定成功后用服务端返回的IBinder对象创建一个Messenger,通过它向服务端发送Message消息。
如果需要服务端给客户端发送消息,需要在Handler的handleMessage方法里,根据客户端发送过来的Message.replyTo获取到客户端的Messenger对象,就可以向客户端发送消息了。同时客户端需要在服务连接的onServiceConnected方法中,将客户端的Messenger对象通过Message.replyTo给收到服务端发送过来的Message对象,这样就实现双方通信。

6.4 AIDL

6.4.1 在服务端新建需要的AIDL类

(1)先新建一个Book.java实现Parcelable接口,表示一个图书的信息类;

import android.os.Parcel;import android.os.Parcelable;/** * Book.java */public class Book implements Parcelable{    private String name;    private int price;    public Book(){}    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public int getPrice() {        return price;    }    public void setPrice(int price) {        this.price = price;    }    public Book(Parcel in) {        name = in.readString();        price = in.readInt();    }    public static final Creator<Book> CREATOR = new Creator<Book>() {        @Override        public Book createFromParcel(Parcel in) {            return new Book(in);        }        @Override        public Book[] newArray(int size) {            return new Book[size];        }    };    @Override    public int describeContents() {        return 0;    }    @Override    public void writeToParcel(Parcel dest, int flags) {        dest.writeString(name);        dest.writeInt(price);    }    /**     * 参数是一个Parcel,用它来存储与传输数据     * @param dest     */    public void readFromParcel(Parcel dest) {        //注意,此处的读值顺序应当是和writeToParcel()方法中一致的        name = dest.readString();        price = dest.readInt();    }    //方便打印数据    @Override    public String toString() {        return "name : " + name + " , price : " + price;    }}

(2) 新建一个Book.aidl,表示Book类在AIDL中的声明;

// Book.aidl//这个文件的作用是引入了一个序列化对象 Book 供其他的AIDL文件使用//注意:Book.aidl与Book.java的包名应当是一样的//注意parcelable是小写parcelable Book;
(3) 新建一个IBookManger.aidl接口,里面包含相应的方法;

// IBookManger.aidl//导入所需要使用的非默认支持数据类型的包import com.lypeer.ipcclient.Book;interface IBookManger {    //所有的返回值前都不需要加任何东西,不管是什么数据类型    List<Book> getBooks();    Book getBook();}
该方法保存后,会在gen目录下生成一个IBookManger.java类,它是系统为BookManger.aidl生成的Binder类,继承android.os.IInterface,它自己也还是个接口。
它包括以下内容:
1) 定义了一个String型的DESCRIPTOR,Binder的唯一标识;
2) asInterface(android.os.IBinder obj)方法,将服务端的Binder对象转换成客户端所需的AIDL接口类型对象,当客户端和服务端处于同一进程,直接返回服务端的Stub对象本身,否则返回系统封装后的Stub.proxy对象。
3) asBinder方法:返回当前Binder对象。
4) onTransact方法:运行在服务端的Binder线程池中,当客户端发起远程请求,远程请求会通过系统封装后交由此方法处理,该方法原型为public Boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags),服务端通过code可以确定客户端请求的是哪一个方法,从data中取出目标方法所需的参数,执行相应方法后,将返回值写入reply中,如果此方法返回false,客户端会请求失败。
5) IBookManger.aidl接口中声明的方法的代理实现,此方法运行在客户端,当客户端调用此方法时,首先需要创建此方法需要的输入类型Parcel对象_data, 输出类型Parcel对象_reply和返回值对象,接着将参数信息写入_data中(若有参数的话),再调用transact方法发起RPC(远程过程调用),当前线程挂起,服务端的onTransact方法会被调用,直到RPC过程返回后,当前线程继续执行,并从_reply中取出RPC过程的返回结果;最后返回_reply中的数据。
6) 声明了IBookManger.aidl中声明的方法;
7) 声明了相应的整型id分别用于标识声明的方法,该id用于标识在transact过程中判断客户端请求的到底是哪个方法;
8) 声明了一个类部类Stub,继承android.os.Binder实现IBookManager.aidl中类接口,当客户端和服务端处于同一进程,方法调用不会走跨进程的transact过程,若位于不同进程,则由Stub的内部代理Proxy,调用跨进程transact过程。
需要注意的是:客户端发起请求时,当前线程会挂起直至服务端返回数据,若方法是一个耗时操作,不能在UI线程中发起此远程请求。服务端的Binder方法运行在Binder线程池中,所以不管操作是否耗时,都应采用同步的方法去实现。
(4)linkToDeath unlinkToDeath
由于Binder运行在服务端进程中,如果服务端进程由于某种原因终止,这时客户端服务端的Binder连接断裂,会导致远程调用失败。但客户端很可能不知道,解决此问题的办法,利用Binder提供的两个配对方法linkToDeath和unlinkToDeath,通过linkToDeath可以给Binder设置一个死亡代理,当Binder被终止时,我们会接收到通知,这时可通过重新发起请求恢复连接。
实现方法:
1) 先声明一个DeathRecipient对象,它是一个接口,内部只有一个方法binderDied,我们需要实现该方法,当Binder死亡时,系统回调binderDied方法,我们可以移出之前binder代理并重新建立远程服务。
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {        @Override        public void binderDied() {            if (mBookManager == null) {                return;            }            mBookManger.asBinder().unlinkToDeath(mDeathRecipient, 0);            mBookManger = null;            //重新绑定远程服务        }    };
2) 在客户端绑定远程服务成功后,给binder设置死亡代理;

mService = IBookManager.Stub.asInterface(binder);binder.linkToDeath(mDeathRecipient, 0);

6.4.2 远程服务端Serivce的实现

(1)Serivce建立
我们需要新建一个Service,称为AIDLService,代码如下:

/** 1. 服务端的AIDLService.java */public class AIDLService extends Service {    public final String TAG = this.getClass().getSimpleName();    //包含Book对象的list    private List<Book> mBooks = new ArrayList<>();    //由AIDL文件生成的IBookManager    private final IBookManager.Stub mBookManager = new IBookManager.Stub() {        @Override        public List<Book> getBooks() throws RemoteException {            synchronized (this) {                Log.e(TAG, "invoking getBooks() method , now the list is : " + mBooks.toString());                if (mBooks != null) {                    return mBooks;                }                return new ArrayList<>();            }        }        @Override        public void addBook(Book book) throws RemoteException {            synchronized (this) {                if (mBooks == null) {                    mBooks = new ArrayList<>();                }                if (book == null) {                    Log.e(TAG, "Book is null in In");                    book = new Book();                }                //尝试修改book的参数,主要是为了观察其到客户端的反馈                book.setPrice(2333);                if (!mBooks.contains(book)) {                    mBooks.add(book);                }                //打印mBooks列表,观察客户端传过来的值                Log.e(TAG, "invoking addBooks() method , now the list is : " + mBooks.toString());            }        }    };    @Override    public void onCreate() {        super.onCreate();        Book book = new Book();        book.setName("Android开发艺术探索");        book.setPrice(28);        mBooks.add(book);       }    @Nullable    @Override    public IBinder onBind(Intent intent) {        Log.e(getClass().getSimpleName(), String.format("on bind,intent = %s", intent.toString()));        return mBookManager;//在onBinder中返回服务端的Binder对象    }}

(2)注册服务

<service android:name="aidl.AIDLService"    android:process=":remote"></service>

6.4.3 客户端的实现

/** 2. 客户端的AIDLActivity.java */public class AIDLActivity extends AppCompatActivity {    //由AIDL文件生成的Java类    private IBookManager mBookManager = null;    //标志当前与服务端连接状况的布尔值,false为未连接,true为连接中    private boolean mBound = false;    //包含Book对象的list    private List<Book> mBooks;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_aidl);    }    /**     * 按钮的点击事件,点击之后调用服务端的addBookIn方法     *     * @param view     */    public void addBook(View view) {        //如果与服务端的连接处于未连接状态,则尝试连接        if (!mBound) {            attemptToBindService();            Toast.makeText(this, "当前与服务端处于未连接状态,正在尝试重连,请稍后再试", Toast.LENGTH_SHORT).show();            return;        }        if (mBookManager == null) return;        Book book = new Book();        book.setName("APP研发录In");        book.setPrice(30);        try {            mBookManager.addBook(book);//通过服务端的Binder对象调服务端方法            Log.e(getLocalClassName(), book.toString());        } catch (RemoteException e) {            e.printStackTrace();        }    }    /**     * 尝试与服务端建立连接     */    private void attemptToBindService() {        Intent intent = new Intent();        intent.setAction("aidl.AIDLService");        intent.setPackage("com.xx.packagename");        bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);//绑定服务    }    @Override    protected void onStart() {        super.onStart();        if (!mBound) {            attemptToBindService();        }    }    @Override    protected void onStop() {        super.onStop();        if (mBound) {            unbindService(mServiceConnection);            mBound = false;        }    }    private ServiceConnection mServiceConnection = new ServiceConnection() {        @Override        public void onServiceConnected(ComponentName name, IBinder service) {            Log.e(getLocalClassName(), "service connected");            mBookManager = IBookManager.Stub.asInterface(service);            mBound = true;            if (mBookManager != null) {                try {                    mBooks = mBookManager.getBooks();//调用服务端的getBooks方法                    Log.e(getLocalClassName(), mBooks.toString());                } catch (RemoteException e) {                    e.printStackTrace();                }            }        }        @Override        public void onServiceDisconnected(ComponentName name) {            Log.e(getLocalClassName(), "service disconnected");            mBound = false;        }    };}

通过获取服务端的Binder对象,就可以和服务端进行通信了。

6.4.4 其他知识点

(1)跨进程listener接口
如果在服务端定声明了接口,客户端进行注册和反注册,会抛出异常,无法反注册,因为在多进程中,Binder会把客户端传过来的对象重新转化成一个新对象,这样虽然客户端注册和反注册使用的是同一个对象,但是通过Binder传递到服务端却变成两个对象,对象的跨进程传输本质都是反序列化过程。
可以使用RemoteCallbackList,它是系统专门用来删除跨进程listener接口。它实现原理利用底层Binder对象是同一个,只要遍历服务端所有listener,找出和解注册listener具有相同Binder对象的服务端listener,并把它删除掉。同时RemoteCallbackList还有一个作用,当客户端进程终止,它会移除客户端所注册的所有listener.
(2)权限验证
在注册服务时添加permission权限验证,或是在onTransact方法中进行权限验证;
(3) AIDL访问流程总结:
先创建一个Service和一个AIDL接口,再创建一个类继承自AIDL接口中的Stub类并实现Stub中的抽象方法;在Service的onBind方法中返回这个对象,再在客户端就可以绑定服务端service,建立连接后就可以访问远程服务端的方法了。

6.5 ContentProvider

和Messenger一样,ContentProvider底层同样是Binder.系统预置了很多ContentProvider,如通讯录、日程表等,只需通过ContentResolver的query/update/insert/delete就可以跨进程访问这些信息。具体实现步骤如下:
1) 新建一个继承自系统ContentProvider的Provider,并重写onCreate, query, getType, insert, delete, update六个方法。
这六个进程运行在ContentProvider的进程中,除了onCreate由系统回调运行在主线程中,其他五个方法运行在Binder线程池中。
2) 注册Provider

<provider android:name=".provider.XXProvider"        android:authorities="com.xx.xx.provider"        android:permission="com.xx.PROVIDER"        android:process=":provider"></provider>
3) 在另一个进程中访问我们定义的ContentProvider

Uri uri = Uri.parse("content://com.xx.xx.provider");getContentResolver().query(uri, null, null, null, null);getContentResolver().query(uri,)

4) ContentProvider数据源发生变化时,可通过ContentResolver的notifyChange方法来通知外界数据发生改变,外界可通过ContentResolver的registerContentObserver方法来注册观察者,通过unregisterContentObserver方法来解除观察者。

6.6 Socket

Socket分为流式套接字和用户数据报套接字,分别是对应于网络的传输控制层中的TCP/UDP
TCP:面向连接的协议,稳定的双向通信功能,连接需要“三次握手”,提供了超时重传机制,具有很高的稳定性;
UDP:面向无连接协议,不稳定的单向通信功能,也可提供双向通信功能。效率高,不能保证数据一定能正确传输。
实现步骤:
1)服务端在新建一个Service,并建立TCP服务

@Override    public void onCreate() {        new Thread(new TcpServer()).start();//阻塞监听客户端连接        super.onCreate();    }    @Override    public IBinder onBind(Intent intent) {        throw null;    }    private class TcpServer implements Runnable {        @Override        public void run() {            ServerSocket serverSocket;            try {                //监听8688端口                serverSocket = new ServerSocket(8688);            } catch (IOException e) {                return;            }            while (!isServiceDestroyed) {                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.println("您好,我是服务端");        while (!isServiceDestroyed) {            String str = in.readLine();//通过数据流的readLine,可直接变成文本格式            Log.i("moon", "收到客户端发来的信息" + str);            if (TextUtils.isEmpty(str)) {                //客户端断开了连接                Log.i("moon", "客户端断开连接");                break;            }            String message = "收到了客户端的信息为:" + str;            // 从客户端收到的消息加工再发送给客户端            out.println(message);        }        out.close();//关闭流        in.close();        client.close();    }

2)注册服务
<service    android:name=".SocketServerService"    android:process=":remote" />
3) 客户端先建立连接服务端socket
Socket socket = null;while (socket == null) {    try {        //选择和服务器相同的端口8688        socket = new Socket("localhost", 8688);        mClientSocket = socket;        mPrintWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);    } catch (IOException e) {        SystemClock.sleep(1000);    }}

4) 客户端和服务端通信,通过while循环不断去读取服务器发送过来的消息

// 接收服务器端的消息BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));while (!isFinishing()) {    final String msg = br.readLine();    if (msg != null) {        runOnUiThread(new Runnable() {                          @Override                          public void run() {                              tv_message.setText(tv_message.getText() + "\n" + "服务端:" + msg);                          }                      }        );    }}

5) 客户端在onCreate中单开线程启动连接服务
Intent service = new Intent(this, SocketServerService.class);        startService(service);        new Thread() {            @Override            public void run() {                connectSocketServer();            }        }.start();

6.7 进程间通信方式比较


0 0
原创粉丝点击