在上一篇文章中,我们学习了Android Service相关的许多重要内容,包括Service的基本用法、Service和Activity进行通信、Service的销毁方式、Service与Thread的关系、以及如何创建前台Service。以上所提到的这些知识点,基本上涵盖了大部分日常开发工作当中可能使用到的Service技术。不过关于Service其实还有一个更加高端的使用技巧没有介绍,即远程Service的用法,使用远程Service甚至可以实现android跨进程通信的功能,下面就让我们具体地学习一下、
在上篇文章中我们知道了,Service其实是运行在主线程里的如果直接在Service中处理一些耗时的逻辑,就会导致程序ANR。
让我们来做个实验验证一下吧,修改上一篇文章中创建的ServiceTest项目,在MyService的onCreate()方法中让线程睡眠60秒,如下所示:
@Override public void onCreate() { super.onCreate(); Log.d(TAG,"onCreate() executed"); try { Thread.sleep(60000); } catch (InterruptedException e) { e.printStackTrace(); } }
重新运行后,点击一下Start Service按钮或Bind Service按钮,程序就会阻塞住并无法进行任何其它操作,过一段时间后就会弹出ANR的提示框,如下图所示:
之前我们提到过,应该在Service中开启线程去执行耗时,这样就可以有效的避免ANR的出现。
那么篇文章的主题是介绍远程Service的用法,如果将MyService转换成一个远程Service,还会不会有ANR的情况呢?让我们来动手尝试一下吧。
将一个普通的Service转换成远程Service其实非常简单,只需要在注册Service的时候将它的android:process属性指定成:remote就可以了,这里:后的可以看成进程名称,可以写别的,不一定要remote,代码如下所示:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.hank.servicetest"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <service android:name=".service.MyService" android:process=":remote" /> </application> </manifest>现在重新运行程序,并点击一下Start Service按钮,你会看到控制台立刻打印了onCreate() executed的信息,(这里要选择进程为remote),而且主界面并没有阻塞住,也不会出现ANR。大概过了一分钟后,又会看到onStartCommand() executed打印出来。
为什么 将MyService转换成远程Service后就不会导致程序ANR了呢?这是由于,使用了远程Service后,MyService已经在另外一个进程当中运行了,所以只会阻塞该进程中的主线程,并不会影响到当前的应用程序
为了证实一下MyService现在确实已经运行在另外一个进程当中了,我们分别在ManiActivity的onCreate()方法和MyService的onCreate()方法里加入一行日志,打印出各自所在的进程id,如下所示:
Log.d("MyService","Process thread id is " + Process.myPid());再次重新运行程序,然后点击一下Start Service按钮,打印结果如下图所示:
可以看到,不仅仅是进程id不丗了,就连应用程序包名也不一样了,Myservice中打印的那条日志,包名后面还跟上了:remote标识。
那既然远程Service这么好用,干脆以后我们把所有的Service都转换成远程Service吧,还省得再开启线程了。其实不然,远程Service非但不好用,甚至可以称得上是较为难用。一般情况下如果可以不使用远程Service,就尽量不要使用它。
下面就来看一下它的弊端吧,首先将MySerivce的onCreate()方法中让线程睡眠的代码去除掉,然后重新运行程序,并点击一下Bind Service按钮,你会发现程序崩溃了?为什么点击Start Service按钮程序就不会崩溃,而点击BindService按钮就崩溃呢?这是由于在Bind Service按钮的点击事件里面我们会让MainActivity和MyService建立关联,但是目前MyService已经是一个远程Service了,Activity和Service运行在两个不同的进程当中,这时就不能再使用传统的建立关联的方式,程序也就崩溃了。
那么如何才能让Activity与一个远程Service建立关联呢?这就要使用AIDL来进行跨进程通信了(IPC)。
AIDL(Android Interface Definition Language)是Android接口定义语言的意思,它可以用于让某个Service与多个应用程序组件之间进行跨进程通信,从而可以实现多个应用程序共享同一个Service的功能。
下面我们就来一步步地看一下AIDL的用法到底是怎样的。首先需要新建一个AIDL文件,在这个文件中定义好Activity需要与Service进行通信的方法,新MyAIDLService.aidl文件,然后菜单选 中Build》Rebulid Project就可以了,代码如下所示:
interface MyAIDLService { int plus(int a ,int b ); String toUpperCase(String str);然后中修改MyService中的代码,在里面实现我们刚刚定义好的MyAIDLService接口,如下所示:
public class MyService extends Service { public static final String TAG = "MyService"; private MyAIDLService.Stub mBinder = new MyAIDLService.Stub() { @Override public int plus(int a, int b) throws RemoteException { return a + b; } @Override public String toUpperCase(String str) throws RemoteException { if (str != null){ return str.toUpperCase(); } return null; } @Override public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException { } }; @Nullable @Override public IBinder onBind(Intent intent) { return mBinder; }
这里先是对MyAIDLService.Stub进行了实现,重写了toUpperCase()和plus()这两个方法,这两个方法的作用分别是将一个字符串全部转换成大写格式,以及将两个传入的整数进行相加 。然后在onBind()方法中将MyAIDLService.Stub的实现返回,这时为什么可以这样写呢?因为Stub其实就是Binder的子类,所以在onBind()方法中可以直接返回Stub的实现。
接下来修改MainActivity中 的代码,如下所示:
public class MainActivity extends AppCompatActivity implements View.OnClickListener { private Button startService; private Button stopService; private Button bindService; private Button unbindService; private MyService.MyBinder myBinder; private MyAIDLService myAIDLService; private ServiceConnection connection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName componentName, IBinder service) { myAIDLService = MyAIDLService.Stub.asInterface(service); try { int result = myAIDLService.plus(10,12); String upperStr = myAIDLService.toUpperCase("hello world"); Log.d("TAG","result is "+result); Log.d("TAG","upperStr is "+upperStr); }catch (RemoteException e){ e.printStackTrace(); } } @Override public void onServiceDisconnected(ComponentName componentName) { } };
我们只是修改了ServiceConnection中的代码,可以看到,这里首先使用了MyAIDLService.Stub.asInterface()方法将传入的IBinder对象 转换成了MyAIDLService对象,接下来就可以调用在MyAIDLService.aidl文件中定义的所有接口了。这里我们先是调用了plus()方法,并传入了两参数,然后调用了toUpperCase()方法,并传入hello world之子符串作为参数,最后将调用方法的返回结果打印出来。
现在重新运行,并点击一下Bind Service按钮,可以看到打印日志如下所示:
由此可见,我们确实已经成功实现跨进程通信了,在一个进程中访问到了另外一个进程中的方法。
不过你也可以看出,目前的跨进程通信其实并没有什么实质上的作用,因为这只是在一个Activity里调整用了同一个应用程序的Service里的方法。而跨进程通信的真正意义昌为了让一个应用程序去访问另一个应用程序中的Service,以实现共享Service的功能。那么下央我们自然要学习一下,如何才能在其它的应用程序中调用到MyService里的方法。
在上一篇文章中我们已经知道,如果想要让Activity与Service之间建立关联,需要调用bindService()方法,并将Intent作为参数传递进去,在Intent里指定好要绑定的Service,示例代码如下:
Intent bindIntent = new Intent(this,MyService.class); bindService(bindIntent,connection,BIND_AUTO_CREATE);
这里在构建Intent的时候是使用MyService.class来指定要绑定哪一个Service的,但是在另一个应用程序中去绑定Service的时候并没有MyService这个类,这时就必须使用到隐式Intent了。
AndroidManifest.xml中的代码,给MyService加上一个action,如下所示:
<service android:name=".service.MyService" android:process=":remote" > <intent-filter> <action android:name="com.example.hank.servicetest.MyAIDLService"/> </intent-filter> </service> </application>
这就说明,MyService可以响应带有com.example.hank.serviceatest.MyAIDLService这个action的Intent。
现在重新运行一下程序,这样就把远程Service端的工作全部完成了。
然后创建一个新Android项目,起名为ClientTest,我们就尝试在这个程序中远程调用MyService中的方法。
ClientTest中的Activity如果想要和MyService建立关联其实也不难,首先需要将MyAIDLService.aidl文件从ServiceTest项目中拷贝过来,注意要将原有的包路径一起拷贝过来,完成后项目结构如下图所示:
然后打开或新建activity_main.xml,在布局文件中也加入一个Bind Service
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Bind Service"/> </LinearLayout>接下来打开或新建MainActivity,在其中加入和MyService建立关联的代码,如下所示:
public class MainActivity extends AppCompatActivity { private MyAIDLService myAIDLService; private ServiceConnection connection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName componentName, IBinder iBinder) { myAIDLService = MyAIDLService.Stub.asInterface(iBinder); try { int result = myAIDLService.plus(50,50); String upperStr = myAIDLService.toUpperCase("comes from ClientTest"); Log.d("TAG","result is "+result); Log.d("TAG","upperStr is "+upperStr); } catch (RemoteException e) { e.printStackTrace(); } } @Override public void onServiceDisconnected(ComponentName componentName) { } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final Button bindService = (Button) findViewById(R.id.but); bindService.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent("com.example.hank.servicetest.MyAIDLService"); intent.setPackage("com.example.hank.servicetest"); bindService(intent,connection, Service.BIND_AUTO_CREATE); } }); } }
这部分代码大家一定会非常眼熟吧?没错,这和在ServiceTest的MainActivity中的代码几乎是完全相同的,只是在让Activity和Service建立关联的时候我们使用隐式intetn,将Intent的action指定成了
com.example.hank.servicetest.MyAIDLService。
在当前Activity和MyService建立关联之后,我们仍然是调用了plus()和toUpperCase()这两个方法,远程的MyService会对传入的参数进行处理并返回结果,然后将结果打印出来。
这样的话,ClientTest中的代码也就全部完成了,现在运行一下这个项目,然后点击Bind Service按钮,此时就会去和远程的MyService建立关联,观察LogCat中的打印信息如下所示:
不过还有一点不需要说明的是,由于这是在不同的进程之间传递数据,Android对这类数据的格式支持是非常有限的,基本上只能传递java的基本数据类型、之子符串、List或Map等,那么如果我想传递一个自定义的类怎么办呢?这就必须让这个类去实现Parcelable接口,并且要给这个类也定义一个同名的AIDL文件。