一. Android IPC 简介
- IPC即Inter-Process Communication,含义为进程间通信或者跨进程通信,是指两个进程之间进行数据交换的过程。
- 线程是CPU调度的最小单元,是一种有限的系统资源。进程一般指一个执行单元,在PC和移动设备上是指一个程序或者应用。进程与线程是包含与被包含的关系。一个进程可以包含多个线程。最简单的情况下一个进程只有一个线程,即主线程(例如Android的UI线程)。
- 任何操作系统都需要有相应的IPC机制。
- 在Android中,IPC的使用场景大概有以下:
- 有些模块由于特殊原因需要运行在单独的进程中。
- 通过多进程来获取多份内存空间。
- 当前应用需要向其他应用获取数据。
二. Android中的多进程模式
为什么要用到多进程?
在Android系统中一个应用默认只有一个进程,每个进程都有自己独立的资源和内存空间,其它进程不能任意访问当前进程的内存和资源,系统给每个进程分配的内存会有限制。如果一个进程占用内存超过了这个内存限制,就会报OOM的问题,很多涉及到大图片的频繁操作或者需要读取一大段数据在内存中使用时,很容易报OOM的问题,为了彻底地解决应用内存的问题,Android引入了多进程的概念,它允许在同一个应用内,为了分担主进程的压力,将占用内存的某些页面单独开一个进程,比如Flash、视频播放页面,频繁绘制的界面等。
Android多进程使用
Android多进程使用很简单,只需要在AndroidManifest.xml的声明四大组件的标签中增加”android:process”属性即可,process分私有进程和全局进程,以“:”号开头的属于私有进程,其他应用组件不可以和他跑在同一个进程中;不以“:”号开头的属于全局进程,其他应用可以通过ShareUID的方式和他跑在同一个进程中;
ips:使用adb shell ps
或adb shell ps|grep 包名
查看当前所存在的进程信息。
Android多进程会带来的问题
所有运行在不同进程的四大组件,只要它们之间需要通过内存在共享数据,都会共享失败。四大组件之间不可能不通过中间层来共享数据。
在多进程模式中,不同进程的组件拥有独立的虚拟机、Application以及内存空间。
- 静态成员和单例模式完全失效。
- 线程同步锁机制完全失效。
这两点都是因为不同进程不在同一个内存空间下,锁的对象也不是同一个对象。 - SharedPreferences的可靠性下降。
SharedPreferences底层是 通过读/写XML文件实现的,并发读/写会导致一定几率的数据丢失。 - Application会多次创建。
由于系统创建新的进程的同时分配独立虚拟机,其实这就是启动一个应用的过程。
避免Android多进程带来的问题
- Intent传递数据。
- 共享文件和SharedPreferences。
- 基于Binder的Messenger和AIDL。
- Socket。
三. IPC基础概念介绍
1、Serializable接口
Serializable
是Java提供的一个序列化接口(空接口),为对象提供标准的序列化和反序列化操作。- 只需要一个类去实现
Serializable
接口并声明一个serialVersionUID
即可实现序列化。 - 如果不手动指定
serialVersionUID
的值,反序列化时当前类有所改变(比如增删了某些成员变量),那么系统就会重新计算当前类的hash值并赋值给serialVersionUID
。这个时候当前类的serialVersionUID
就和序列化数据中的serialVersionUID
不一致,导致反序列化失败,程序就出现crash。 - 静态成员变量属于类不属于对象,不参与序列化过程,其次
transient
关键字标记的成员变量不参与序列化过程
2、Parcelable接口
- Parcelable内部包装了可序列化的数据。
- 序列化功能由writeToParcel方法完成,最终是通过Parcel的一系列writer方法来完成
@Override public void writeToParcel(Parcel out, int flags) { out.writeInt(code); out.writeString(name); }
- 反序列化功能由CREATOR来完成,其内部表明了如何创建序列化对象和数组,通过Parcel的一系列read方法来完成。
public static final Creator CREATOR = new Creator() { @Override public Book createFromParcel(Parcel in) { return new Book(in); } @Override public Book[] newArray(int size) { return new Book[size]; } }; protected Book(Parcel in) { code = in.readInt(); name = in.readString(); }
- 内容描述功能由describeContents方法完成,几乎所有情况下都应该返回0,仅当当前对象中存在文件描述符时返回1。
public int describeContents() { return 0; }
- Serializable是Java的序列化接口,使用简单但开销大,序列化和反序列化过程需要大量I/O操作。而Parcelable是Android中的序列化方式,适合在Android平台使用,效率高但是使用麻烦。Parcelable主要在内存序列化上,Parcelable也可以将对象序列化到存储设备中或者将对象序列化后通过网络传输,但是稍显复杂,推荐使用Parcelable。
3、Binder
- Binder是Android中的一个类,实现了
IBinder
接口。从IPC角度说,Binder是Andoird的一种跨进程通讯方式。从Android Framework角度来说,Binder是ServiceManager
连接各种Manager(ActivityManager·
、WindowManager
)和相应ManagerService
的桥梁。从Android应用层来说,Binder是客户端和服务端进行通信的媒介,当bindService时,服务端返回一个包含服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务器端提供的服务或者数据(包括普通服务和基于AIDL的服务)。 - Android中Binder主要用于
Service
,包括AIDL和Messenger。普通Service的Binder不涉及进程间通信,Messenger的底层其实是AIDL,所以下面通过AIDL分析Binder的工作机制。
Binder机制原理图:
3.1由系统根据AIDL文件自动生成.java文件
-
- Book.java
表示图书信息的实体类,实现了Parcelable接口。 - Book.aidl
Book类在AIDL中的声明。package com.aoaoyi.ipc.aidl; parcelable Book;
- IBookManager.aidl
定义的管理Book实体的一个接口,包含getBookList和addBook两个方法。
系统为IBookManager.aidl生产的Binder类,在gen目录下的IBookManager.java类。
IBookManager继承了IInterface接口,所有在Binder中传输的接口都需要继承IInterface接口。结构如下:
- Book.java
- 声明了getBookList和addBook方法,还声明了两个整型id分别标识这两个方法,用于标识在transact过程中客户端请求的到底是哪个方法。
- 声明了一个内部类Stub,这个Stub就是一个Binder类,当客户端和服务端位于同一进程时,方法调用不会走跨进程的transact。当二者位于不同进程时,方法调用需要走transact过程,这个逻辑有Stub的内部代理类Proxy来完成。
- 这个接口的核心实现就是它的内部类Stub和Stub的内部代理类Proxy。
3.2 Stub和Proxy类的内部方法和定义
- DESCRIPTOR
Binder的唯一标识,一般用Binder的类名表示。 - asInterface(android.os.IBinder obj)
将服务端的Binder对象转换为客户端所需的AIDL接口类型的对象,如果C/S位于同一进程,此方法返回就是服务端的Stub对象本身,否则返回的就是系统封装后的Stub.proxy对象。 - asBinder
返回当前Binder对象。 - onTransact
这个方法运行在服务端的Binder线程池中,由客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法来处理。该方法的原型是
java public Boolean onTransact(int code,Parcelable data,Parcelable reply,int flags)- 服务端通过code确定客户端请求的目标方法是什么
- 接着从data取出目标方法所需的参数,然后执行目标方法。
- 执行完毕后向reply写入返回值(如果有返回值)。
- 如果这个方法返回值为false,那么服务端的请求会失败,利用这个特性我们可以来做权限验证。
- Proxy#getBookList 和Proxy#addBook
- 这个方法运行在客户端,首先该方法所需要的输入型对象Parcel对象_data,输出型Parcel对象_reply和返回值对象List。
- 然后把该方法的参数信息写入data(如果有参数)
- 接着调用transact方法发起RPC(远程过程调用),同时当前线程挂起
- 然后服务端的onTransact方法会被调用知道RPC过程返回后,当前线程继续执行,并从reply中取出RPC过程的返回结果,最后返回reply中的数据。
之所以提供AIDL文件,是为了方便系统为我们生成代码,我们完全可以自己实现Binder
3.3可以给Binder设置一个死亡代理,当Binder死亡时,就会收到通知。
- 声明一个DeathRecipient对象。DeathRecipient只有一个方法binderDied,当Binder死亡的时候,系统就会回调DeathRecipient方法。
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient(){ @Override public void binderDied(){ if(mBookManager == null){ return; } mBookManager.asBinder().unlinkToDeath(mDeathRecipient,0); mBookManager = null; // TODO:接下来重新绑定远程Service } }
- Binder有两个很重要的方法linkToDeath和unlinkToDeath。通过linkToDeath为Binder设置一个死亡代理。
mService = IBookManager.Stub.asInterface(binder); binder.linkToDeath(mDeathRecipient,0);
- 另外,可以通过Binder的isBinderAlive判断Binder是否死亡。
四. Android中的IPC方式
主要有以下方式:
- Intent中附加extras来传递消息
- 共享文件
- Binder方式
- 四大组件之一的ContentProvider
- 四大组件之一的Broadcast
- Socket
名称 | 有点 | 缺点 | 适用场景 |
---|---|---|---|
Bundle | 简单易用 | 只能传输Bundle支持的数据类型 | 四大组件间的进程通信 |
文件共享 | 简单易用 | 不适合高并发场景,并且无法做到进程间的即时通信 | 无并发访问情景,交换简单的数据及时性不高的场景 |
AIDL | 功能强大,支持一对多并发通信,支持即时通信 | 使用复杂,需要处理好线程同步 | 一对多通信且有RPC(Remote Procedure Call)需求 |
Messenger | 功能一般,支持一对多串行通信,支持即时通信 | 不能很好的处理高并发情形,不支持PRC,数据通过Message进行传输,因此只能传输Bundle支持的数据类型 | 低并发的一对多及时通信,无RPC需求,或无需返回结果的RPC需求 |
ContextProdiver | 在数据源访问方便功能强大,支持一对多并发数据共享,可以通过Call方法进行扩展其他操作 | 可以理解为受约束的AIDL,主要提供数据源的CRUD操作 | 一对多的进程间数据共享 |
Broadcast | 功能一般,支持一对多串行通信,支持即时通信 | 只能传输Bundle支持的数据类型,占用的系统资源比较多 | 一对多的进程间数据传递 |
Socket | 功能强大,可以通过网络传输字节流,支持一对多并发实时通信 | 实现细节稍微有点繁琐,不只支持直接的RPC | 网络数据交互 |
1.使用Bundle的方式
2:String和CharSequence
3:List:只支持ArrayList,并且里面的元素都能被AIDL支持
4:Map:只支持HashMap,里面的每个元素能被AIDL支持
5:Parcelable:所有实现Parcelable接口的对象
下面看一个Demo例子:利用Bundle进行进程间通信
Intent intent = new Intent(MainActivity.this, TwoActivity.class);
Bundle bundle = new Bundle();
bundle.putString("data", "测试数据");
intent.putExtras(bundle);
startActivity(intent);
注意:利用Bundle进行进程间通信是很容易的,大家应该注意到,这种方式进行进程间通信只能是单方向的简单数据传输,它使用时有一定的局限性。
2.使用文件共享的方式
举个例子:
在A进程中创建一个线程进行写数据:
new Thread(new Runnable() {
@Override
public void run() {
User user = new User(1, "user", false);
File cachedFile = new File(CACHE_FILE_PATH);
ObjectOutputStream objectOutputStream = null;
try{
objectOutputStream = new ObjectOutputStream
(new FileOutputStream(cachedFile));
objectOutputStream.writeObject(user);
}catch(IOException e){
e.printStackTrace();
}finally{
objectOutputStream.close();
}
}
}).start();
在B进程中创建一个线程进行读取数据:
new Thread(new Runnable() {
@Override
public void run() {
User user = null;
File cachedFile = new File(CACHE_FILE_PATH);
if(cachedFile.exists()){
ObjectInputStream objectInputStream = null;
try{
objectInputStream = new ObjectInputStream
(new FileInputStream(cachedFile));
user = objectInputStream.readObject(user);
}catch(IOException e){
e.printStackTrace();
}finally{
objectInputStream.close();
}
}
try{
objectOutputStream = new ObjectOutputStream
(new FileOutputStream(cachedFile));
objectOutputStream.writeObject(user);
}catch(IOException e){
e.printStackTrace();
}finally{
objectOutputStream.close();
}
}
}).start();
通过文件共享的这种方式来共享数据对文件的格式是有具体要求的,比如可以是文本文件,也可以是XML文件,只要读写双方约定数据格式即可。这种方式进行进程间通信虽然方便,可是也是有局限性的,比如并发读/写,这会导致比较严重的问题,如读取的数据不完整或者读取的数据不是最新的。因此通过文件共享的方式适合在数据同步要求不高的进程间通信,并且要妥善处理并发读/写问题。
SharedPreferences底层实现采用XML文件来存储键值对。系统对它的读/写有一定的缓存策略,即在内存中会有一份SharedPreferences文件的缓存,因此在多进程模式下,系统对它的读/写变得不可靠,面对高并发读/写时SharedPreferences有很大几率丢失数据,因此不建议在IPC中使用SharedPreferences。
3.使用Messenger的方式
Messenger的使用方法也是比较简单的,实现一个Messenger有以下几步,分为服务器端和客服端:
服务器进程:在A进程创建一个Service来处理其他进程的连接请求,同时创建一个Handler并通过它来创建一个Messenger对象,然后在Service的onBind()中返回这个Messenger对象底层的Binder即可。
public class MessengerService extends Service{
private Handler MessengerHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
//消息处理.......
};
//创建服务端Messenger
private final Messenger mMessenger = new Messenger(MessengerHandler);
@Override
public IBinder onBind(Intent intent) {
//向客户端返回Ibinder对象,客户端利用该对象访问服务端
return mMessenger.getBinder();
}
@Override
public void onCreate() {
super.onCreate();
}
}
客户端进程:在进程B中首先绑定远程进程Service,绑定成功后,根据Service返回的IBinder对象创建Messenger对象,并使用此对象发送消息,为了能收到Service端返回的消息,客户端也创建了一个自己的Messenger发送给Service端,Service端就可以通过客户端的Messenger向客户端发送消息了,具体的实现代码如下:
public class MessengerActivity extends Activity{
private ServiceConnection conn = new ServiceConnection(){
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//根据得到的IBinder对象创建Messenger
mService = new Messenger(service);
//通过得到的mService 可以进行通信
}
};
//为了收到Service的回复,客户端需要创建一个接收消息的Messenger和Handler
private Handler MessengerHander = new Handler(){
@Override
public void handleMessage(Message msg) {
//消息处理
}
};
private Messenger mGetMessenger = new Messenger(MessengerHander);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_messenger);
init();
}
private void init() {
intent = new Intent(MessengerActivity.this, MessengerService.class);
indService(intent, conn, Context.BIND_AUTO_CREATE);
}
@Override
protected void onDestroy(){
unbindService(conn);
super.onDestroy();
}
}
下面给出一张Messenger的工作原理图,以便于更好的理解Messenger:
Messenger内部消息处理使用Handler实现的,所以它是以串行的方式处理客服端发送过来的消息的,如果有大量的消息发送给服务器端,服务器端只能一个一个处理,如果并发量大的话用Messenger就不合适了,而且Messenger的主要作用就是为了传递消息,很多时候我们需要跨进程调用服务器端的方法,这种需求Messenger就无法做到了。
4.使用AIDL的方式
AIDL(Android Interface Definition Language)是一种IDL语言,用于生成可以在Android设备上两个进程之间进行进程间通信(IPC)的代码。如果在一个进程中(例如Activity)要调用另一个进程中(例如Service)对象的操作,就可以使用AIDL生成可序列化的参数。
AIDL是IPC的一个轻量级实现,用了对于Java开发者来说很熟悉的语法。Android也提供了一个工具,可以自动创建Stub(类架构,类骨架)。当我们需要在应用间通信时,我们需要按以下几步走:
1:定义一个AIDL接口。
2:为远程服务(Service)实现对应Stub。
3:将服务“暴露”给客户程序使用。
“只有当你允许来自不同的客户端访问你的服务并且需要处理多线程问题时你才必须使用AIDL”,其他情况下你都可以选择其他方法,如使用Messenger,也能跨进程通信。可见AIDL是处理多线程、多客户端并发访问的。而Messenger是单线程处理。
AIDL很大的好处就是我们直接可以调用服务端进程所暴露出来的方法,下面简单介绍一下使用AIDL的使用方法:
服务端:
(1):创建aidl接口文件
AIDL使用简单的语法来声明接口,描述其方法以及方法的参数和返回值。这些参数和返回值可以是任何类型的,甚至是其他AIDL生成的接口。重要的是必须导入所有非内置类型,哪怕是这些类型是与接口相同的包中。
package com.aoaoyi.android;
interface IRemoteService {
int getPid();
void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString);
}
(2):向客户端暴露接口
public class DDService extends Service {
@Override
public void onCreate() {
super.onCreate();
System.out.println("DDService onCreate........"
+ "Thread: " + Thread.currentThread().getName());
}
@Override
public IBinder onBind(Intent arg0) {
System.out.println("DDService onBind");
return mBinder;
}
private final IRemoteService.Stub mBinder = new IRemoteService.Stub() {
public int getPid(){
System.out.println("Thread: " + Thread.currentThread()
.getName());
System.out.println("DDService getPid ");
return Process.myPid();
}
public void basicTypes(int anInt, long aLong, boolean aBoolean,
float aFloat, double aDouble, String aString) {
System.out.println("Thread: " + Thread.currentThread().getName());
System.out.println("basicTypes aDouble: " + aDouble
+" anInt: " + anInt+" aBoolean " + aBoolean
+" aString " + aString);
}
};
}
这样我们的服务器端就完成了,把服务器端运行到手机上,等一会可以看一下打印信息。重点看“线程名”。
客户端:
客户端所做的事情就要简单很多了,首先需要绑定服务器端Service,绑定成功后将服务器端返回的Binder对象转成AIDL接口所属的类型,接着皆可以调用AIDL中的方法了。
public class MainActivity extends Activity {
private IRemoteService remoteService;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
ServiceConnection conn = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
remoteService = IRemoteService.Stub.asInterface(service);
try {
int pid = remoteService.getPid();
int currentPid = Process.myPid();
System.out.println("currentPID: "
+ currentPid +" remotePID: " + pid);
remoteService.basicTypes(12, 1223, true, 12.2f, 12.3,
"我们的爱,我明白");
} catch (RemoteException e) {
e.printStackTrace();
}
System.out.println("bind success! "
+ remoteService.toString());
}
};
/**
* 监听按钮点击
* @param view
*/
public void buttonClick(View view) {
System.out.println("begin bindService");
Intent intent = new Intent("duanqing.test.aidl");
bindService(intent, conn, Context.BIND_AUTO_CREATE);
}
@Override
protected void onDestroy() {
super.onDestroy();
unbindService(conn);
}
}
这样就实现了AIDL进行进程间通信了,是不是也很简单,不过这个看似简单,其实底层Android为我们做了很多的事情,核心就是Binder,感兴趣的读者可以学习一下Binder原理。
注意事项:
- AIDL支持的数据类型:
- 基本数据类型、String、CharSequence
- List:只支持ArrayList,里面的每个元素必须被AIDL支持
- Map:只支持HashMap,里面的每个元素必须被AIDL支持
- Parcelable
- 所有的AIDL接口本身也可以在AIDL文件中使用
- 自定义的Parcelable对象和AIDL对象,不管它们与当前的AIDL文件是否位于同一个包,都必须显式import进来。
- 如果AIDL文件中使用了自定义的Parcelable对象,就必须新建一个和它同名的AIDL文件,并在其中声明它为Parcelable类型。
package com.aoaoyi.ipc.aidl; parcelable Book;
- AIDL接口中的参数除了基本类型以外都必须表明方向in/out。AIDL接口文件中只支持方法,不支持声明静态常量。建议把所有和AIDL相关的类和文件放在同一个包中,方便管理。
void addBook(in Book book);
- AIDL方法是在服务端的Binder线程池中执行的,因此当多个客户端同时连接时,管理数据的集合直接采用CopyOnWriteArrayList来进行自动线程同步。类似的还有ConcurrentHashMap。
- 因为客户端的listener和服务端的listener不是同一个对象,所以RecmoteCallbackList是系统专门提供用于删除跨进程listener的接口,支持管理任意的AIDL接口,因为所有AIDL接口都继承自IInterface接口。
public class RemoteCallbackList
它内部通过一个Map接口来保存所有的AIDL回调,这个Map的key是IBinder类型,value是Callback类型。当客户端解除注册时,遍历服务端所有listener,找到和客户端listener具有相同Binder对象的服务端listenr并把它删掉。
- 客户端RPC的时候线程会被挂起,由于被调用的方法运行在服务端的Binder线程池中,可能很耗时,不能在主线程中去调用服务端的方法。
5.使用ContentProvider
- ContentProvider是四大组件之一,其底层实现和Messenger一样是Binder。ContentProvider天生就是用来进程间通信,只需要实现一个自定义或者系统预设置的ContentProvider,通过ContentResolver的query、update、insert和delete方法即可。
- 创建ContentProvider,只需继承ContentProvider实现
onCreate
、query
、update
、insert
、getType
六个抽象方法即可。除了onCreate
由系统回调并运行在主线程,其他五个方法都由外界调用并运行在Binder线程池中。
(1):定义自己的ContentProvider类,该类集成ContentProvider基类;
(2):在AndroidMainfest.xml中注册这个ContentProvider,类似于Activity注册,注册时要给ContentProvider绑定一个域名;
(3):当我们注册好这个ContentProvider后,其他应用就可以访问ContentProvider暴露出来的数据了。
ContentProvider只是暴露出来可供其他应用操作的数据,其他应用则需要通过ContentProvider来操作ContentProvider所暴露出来的数据。Content提供了getContentResolver()方法来获取ContentProvider对象,获取之后皆可以对暴露出来的数据进行增、删、改、查操作了。
使用ContentResolver操作数据的步骤也很简单:
(1)调用Activity的getContentResolver()获取ContentResolver对象;
(2)根据调用的ContentResolver的insert()、delete()、update()和query()方法操作数据库即可。
6. 使用广播接收者(Broadcast)的方式
广播是一种被动跨进程通信方式。当某个程序向系统发送广播时,其他的应用程序只能被动地接收广播数据。这就像电台进行广播一样,听众只能被动地收听,而不能主动与电台进行沟通。
BroadcastReceiver本质上是一个系统级的监听器,它专门监听各个程序发出的Broadcast,因此它拥有自己的进程,只要存在与之匹配的Intent被广播出来,BroadcastReceivert总会被激发。我们知道,只要注册了某个广播之后,广播接收者才能收到该广播。广播注册的一个行为是将自己感兴趣的IntentFilter注册到Android系统的AMS(ActivityManagerService)中,里面保存了一个IntentFilter列表。广播发送者将IntentFilter的action行为发送到AMS中,然后遍历AMS中的IntentFilter列表,看谁订阅了该广播,然后将消息遍历发送到注册了相应的IntentFilter或者Service中---也就是说:会调用抽象方法onReceive()方法。其中AMS起到了中间桥梁的作用。
程序启动BroadcastReceiver只需要两步:
(1):创建需要启动的BroadcastReceivert的intent;
(2):调用Context的sendBroadcast()或者sendOrderBroadcast()方法来启动指定的BroadcastReceivert。
每当Broadcast事件发生后,系统会创建对应的BroadcastReceiver实例,并自动触发onReceiver()方法,onReceiver()方法执行完后,BroadcastReceiver实例就会被销毁。
注意:onReceiver()方法中尽量不要做耗时操作,如果onReceiver()方法不能再10秒之内完成事件的处理,Android会认为该进程无响应,也就弹出我们熟悉的ANR对话框。如果我们需要在接收到广播消息后进行耗时的操作,我们可以考虑通过Intent启动一个Server来完成操作,不应该启动一个新线程来完成操作,因为BroadcastReceiver生命周期很短,可能新建线程还没有执行完,BroadcastReceivert已经销毁了,而如果BroadcastReceivert结束了,它所在的进程中虽然还有启动的新线程执行任务,可是由于该进程中已经没有任何组件,因此系统会在内存紧张的情况下回收该进程,这就导致BroadcastReceivert启动的子线程不能执行完成。
7.使用Socket的方式
Socket
对象就可以向服务端发送消息或者接受服务端发送的消息。
文章评论