Android-Service入门到崩溃

最近项目搞完了,有点空正好查漏补缺关于服务这一块,一直都没怎么用过,趁着这个时机学习一下至于为啥起这名呢……因为本来就想着稍微看一下Service,结果发现要看的东西越来越多……越来越多……所以就有崩了的意思……本文代码比较多,请边看边敲,碰到不懂去搜一下。

首先梳理一下我想要写的东西:

  • Service基础
  • AIDL

什么是Service

Service是一个可以在后台执行长时间运行操作而不提供用户界面的应用组件。服务可由其他应用组件启动,而且即使用户切换到其他应用,服务仍将在后台继续运行。服务并不是一个单独的进程,除非有特殊指明,否则Service是作为application的一部分运行在相同的进程中的。服务没有分离于主线程工作,所以当你要在服务中进行耗时操作最好开启一个新的线程来执行这种操作,避免ANR。

启动Service的两种方式

有两种方式启动service,一种是startService,一种是bindService,暂时从service的生命周期上来看一下这两种有何异同,先贴代码:
Service:

package com.xiasuhuei321.client;

import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.util.Log;

/**
 * Created by xiasuhuei321 on 2017/1/11.
 * author:luo
 * e-mail:[email protected]
 */

public class TestService extends Service {
    public static final String TAG = "TestService";

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        Log.e(TAG, "onBind");
        return null;
    }

    @Override
    public void onCreate() {
        Log.e(TAG, "onCreate");
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.e(TAG, "onStartCommand");
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onRebind(Intent intent) {
        Log.e(TAG, "onRebind");
    }

    @Override
    public void onDestroy() {
        Log.e(TAG, "onDestroy");
    }

    @Override
    public boolean onUnbind(Intent intent) {
        Log.e(TAG, "onUnbind");
        return super.onUnbind(intent);
    }

    private static class MyBinder extends Binder{

    }
}

Activity:

package com.xiasuhuei321.client;

import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private Intent startIntent;
    private Intent bindIntent;

    public static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.bt_bind_service).setOnClickListener(this);
        findViewById(R.id.bt_start_service).setOnClickListener(this);
        findViewById(R.id.bt_stop_service).setOnClickListener(this);
        findViewById(R.id.bt_unbind_service).setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.bt_bind_service:
                bindIntent = new Intent(this, TestService.class);
                bindService(bindIntent, connection, BIND_AUTO_CREATE);
                break;

            case R.id.bt_start_service:
                startIntent = new Intent(this, TestService.class);
                startService(startIntent);
                break;

            case R.id.bt_stop_service:
                if (startIntent != null)
                    stopService(startIntent);
                break;

            case R.id.bt_unbind_service:
                if(bindIntent != null)
                    unbindService(connection);
                break;
        }
    }

    ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
            Log.e(TAG, "onServiceConnected");

        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {
            Log.e(TAG, "onServiceDisconnected");
        }
    };
}

xml文件就不贴了,就是四个按钮,从上到下分别是bindService、startService、unbindService、stopService两两对应,样子如下所示:

Paste_Image.png

最后别忘了在清单文件中注册一下,android四大组件都需要注册的。

使用bindService方法启动时service的生命周期:

service

点击unbind按钮时service的生命周期:

service

点击startService

service

点击stopService

service

通过以上的生命周期我们会发现,通过bindService和startService启动的服务生命周期会有所不同,其实bindService还有另外的东西,不过因为我在onBind()方法里返回了null所以没有体现出来。那么我返回一个他要的试试看,修改的代码只有TestService和ServiceConnection,只放修改的代码:

package com.xiasuhuei321.client;

import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.util.Log;

/**
 * Created by xiasuhuei321 on 2017/1/11.
 * author:luo
 * e-mail:[email protected]
 */

public class TestService extends Service {
    public static final String TAG = "TestService";

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        Log.e(TAG, "onBind");
        return new MyBinder();
    }

    @Override
    public void onCreate() {
        Log.e(TAG, "onCreate");
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.e(TAG, "onStartCommand");
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onRebind(Intent intent) {
        Log.e(TAG, "onRebind");
    }

    @Override
    public void onDestroy() {
        Log.e(TAG, "onDestroy");
    }

    @Override
    public boolean onUnbind(Intent intent) {
        Log.e(TAG, "onUnbind");
        return super.onUnbind(intent);
    }

    public static class MyBinder extends Binder {
        public void playMusic() {
            Log.e("MyBinder", "play music");
        }
    }
}
    // 在MainActivity中
    ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
            Log.e(TAG, "onServiceConnected");
            TestService.MyBinder mb = (TestService.MyBinder) iBinder;
            mb.playMusic();
        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {
            Log.e(TAG, "onServiceDisconnected");
        }
    };

那么让咱点下BIND_SERVICE按钮看看有啥反应:

service

可以看到,通过ServiceConnection中有两个回调方法,其中一个带有IBinder类型的对象,而我强转成我自己继承自Binder的MyBinder也没有错。可以猜测这里的IBinder就是我在onBind()里返回的MyBinder,而我可以在这个类里写一个方法,再通过拿到的IBinder来调用这个方法。

小结:

onCreate方法只在首次创建服务时,系统将调用此方法来执行一次性设置程序(在调用 onStartCommand()或onBind()之前)。如果服务已在运行,则不会调用此方法。点击BIND_SERVIC和START_SERVICE(即bindService和startService都调用),再点击UNBIND_SERVICE或STOP_SERVICE实际上都不会调用onDestroy方法。因为service可以和多个客户端绑定(不过只在第一次和客户端绑定的时候调用onBind方法,随后即可将同一IBinder传递给其他绑定的客户端),除非所有的客户端均取消绑定(如果通过startService也启动了这个服务,那么还得stopService才会停止),否则stopService或stopSelf不会实际停止服务。

在阅读鸿洋大神的一篇AIDL文章时,我尝试了下他的代码,发现使用隐式Intent启动服务会报错,后来搜索发现说是Android 5.0以上只能显示启动服务了,解决方案后文和网上都有。

创建绑定服务的三种方式

创建绑定服务时必须提供IBinder,提供客户端和服务交互的“接口”。这里的接口并非java语言中的interface,可以理解为提供客户端调用的方法之类的。android提供三种方式:

  • 扩展(继承)Binder类
  • 使用Messenger
  • 使用AIDL

第一种扩展Binder类在Android API指南中描述如下:如果服务是供你的自由应用专用,并且在客户端相同的进程中运行(常见情况),则应通过扩展Binder类并从onBind()返回它的一个实例来创建接口。客户端收到Binder后,可利用它直接访问Binder实现中乃至Service中可用的公共方法。

如果服务只是您的自有应用的后台工作线程,则优先采用这种方法。不以这种方式创建接口的唯一原因是,您的服务被其他应用或不同的进程占用。

也就是说如果不是为了跨进程而用其他两种方式你就是在耍流氓~

耍流氓

AIDL

在正式了解AIDL之前,我觉得有必要先弄明白线程和进程的概念。首先看一下他们正式的定义:

  • 线程(Thread)是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。

  • 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础

从我们平时的开发中可以这么阐述进程与线程:进程是程序的一次运行,线程是其中一段代码的执行。而我们平时写的Android程序一般都是在一个进程中运行的,进程名即包名。Android系统内核是Linux内核,而每一个应用程序都运行在一个虚拟机上,每个应用程序都是一个进程。在Android中一个应用程序也是可以使用多进程的,其方式非常简单,只要在清单文件中指定对应的组件属性:

android:process=":remote"

这样就可以了,但是本来一个问题你可以用多进程来解决,然后你用了,之后你就会有两个问题了。说笑,多进程会带来的问题就是内存不共享,因为每个进程都独享自己的一块内存,没办法直接互相访问内存。一个进程中已经存在对象另外一个进程中不能直接使用,不过好在Android提供了很多方式给我们“跨进程”,AIDL便是我们首先要探究的一种方式。

AIDL小例子

我看了几篇文的例子,《Android开发艺术探索》、鸿洋大神和网上其他人写的,感觉还是先用鸿洋大神的例子好一点,鸿洋大神的例子是简单的计算两个整型数值的相加和相减的结果。我们首先新建一个AIDL文件:

新建AIDL

AIDL文件代码如下:

// Calc.aidl
package com.xiasuhuei321.forjianshu;

// Declare any non-default types here with import statements

interface CalcManage {
    int plus(int a,int b);
    int min(int a,int b);
}

然后点击Android Studio的make project

make pro

完成之后会在项目的app/build/generated/source/aidl/debug下生成一个java文件,这里和鸿洋大神一样,具体的解释放到之后再说,我们先走完这个流程。

接着新建一个CalcService的Java类:

package com.xiasuhuei321.forjianshu;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.annotation.Nullable;

/**
 * Created by xiasuhuei321 on 2017/1/13.
 * author:luo
 * e-mail:[email protected]
 */

public class CalcService extends Service {
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }

    CalcManage.Stub binder = new CalcManage.Stub() {
        @Override
        public int plus(int a, int b) throws RemoteException {
            return a + b;
        }

        @Override
        public int min(int a, int b) throws RemoteException {
            return a - b;
        }
    };

}

可以看到我这里只是返回了一个binder,不过这个binder是我们通过AIDL生成的java文件中的一个类,恩,下文再解释。

接着是主界面的布局,和MainActivity的代码,恩因为能直接用鸿洋大神的。。直接copy过来的。。。各位不要学我。。。

不要学

布局:




    

MainActivity:

package com.xiasuhuei321.forjianshu;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Toast;

import java.util.List;

public class MainActivity extends AppCompatActivity {

    private CalcManage mCalcAidl;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    /**
     * 客户端主要通过ServiceConnected与服务端连接
     */
    private ServiceConnection mServiceConn = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Log.e("client", "onServiceConnected");
            mCalcAidl = CalcManage.Stub.asInterface(service);

        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.e("client", "onServiceDisconnected");
            mCalcAidl = null;
        }
    };

    /**
     * 点击BindService按钮时调用
     *
     * @param v
     */
    public void bindService(View v) {
        Intent intent = new Intent();
        intent.setAction("com.xiasuhuei321.forjianshu.calc");
        // 在5.0以上隐式启动绑定service会抛异常
        final Intent i = new Intent(createExplicitFromImplicitIntent(this,intent));
        bindService(i, mServiceConn, Context.BIND_AUTO_CREATE);
    }

    /**
     * 点击unBindService按钮时调用
     */
    public void unbindService(View v) {
        unbindService(mServiceConn);
    }

    /**
     * 点击12 + 12按钮时调用
     */
    public void addInvoked(View v) throws Exception {
        if (mCalcAidl != null) {
            int addRes = mCalcAidl.plus(12, 12);
            Toast.makeText(this, addRes + "", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, "服务器被异常杀死,请重新绑定服务端", Toast.LENGTH_SHORT).show();
        }
    }

    /**
     * 点击50 - 12按钮时调用
     */
    public void minInvoked(View v) throws RemoteException {
        if (mCalcAidl != null) {
            int addRes = mCalcAidl.min(50, 12);
            Toast.makeText(this, addRes + "", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, "服务端未绑定或被异常杀死,请重新绑定服务端", Toast.LENGTH_SHORT).show();
        }
    }

    /***
     * Android L (lollipop, API 21) introduced a new problem when trying to invoke implicit intent,
     * "java.lang.IllegalArgumentException: Service Intent must be explicit"
     *
     * If you are using an implicit intent, and know only 1 target would answer this intent,
     * This method will help you turn the implicit intent into the explicit form.
     *
     * Inspired from SO answer: http://stackoverflow.com/a/26318757/1446466
     * @param context
     * @param implicitIntent - The original implicit intent
     * @return Explicit Intent created from the implicit original intent
     */
    public static Intent createExplicitFromImplicitIntent(Context context, Intent implicitIntent) {
        // Retrieve all services that can match the given intent
        PackageManager pm = context.getPackageManager();
        List resolveInfo = pm.queryIntentServices(implicitIntent, 0);

        // Make sure only one match was found
        if (resolveInfo == null || resolveInfo.size() != 1) {
            return null;
        }

        // Get component info and create ComponentName
        ResolveInfo serviceInfo = resolveInfo.get(0);
        String packageName = serviceInfo.serviceInfo.packageName;
        String className = serviceInfo.serviceInfo.name;
        ComponentName component = new ComponentName(packageName, className);

        // Create a new intent. Use the old one for extras and such reuse
        Intent explicitIntent = new Intent(implicitIntent);

        // Set the component to be explicit
        explicitIntent.setComponent(component);

        return explicitIntent;
    }
}

最后不要忘了在清单文件中注册这个service:

        
            
                

                
            
        

最后来看一下结果如何:


结果

一些值得注意的地方已经在代码中注释了,看结果很明显是对的,但是我这里还没有体现出来AIDL跨进程跨在哪了,接下来就给这个service指定一个进程,看一下还能跑不。

修改一下清单文件中service的属性:

        
            
                

                
            
        

启动之后点击第一个按钮绑定服务,再查看果然多了:calc进程。

两个进程

还能跑不?


结果

结果是一样的,正常运行,这说明了AIDL的确能实现“跨进程”。这种方式也可以用于不同app间的通信,只不过可能会麻烦一点。接下来就新建一个client module:

新建一个module

现在我们新建的这个module将之称为client,而之前的项目将之称为server。现在我们将在server通过AIDL文件生成的java文件复制过来

复制

当然了,因为包更改了,肯定一堆问题,改过来就好了,将该文件内的包名全部修改对了就可以了。然后,恩,把之前的布局和能用到的都复制过来……布局不怎么需要改,MainActivity的代码也先不该,复制过来跑起来看看咋样。

结果肯定是闪退了~别问我咋知道,如果你报的错是以下这种:

错误

那说明CalcMange代码里的DESCRIPTOR错了,毕竟直接拷贝过来的。。。各种错不稀奇。。

private static final String DESCRIPTOR = "com.xiasuhuei321.client.CalcManage";

因为刚全局修改了包名,所以这里也改了,但是这不能改,不然就会抛这个异常。

private static final String DESCRIPTOR = "com.xiasuhuei321.forjianshu.CalcManage";

好了,改成这样再来一遍,看一下结果:

结果

分析

正确的得出了结果,恩,试也试完了,接下来研究一下AIDL为啥能做到跨进程。前面一直拖着说道后面解释的玩意,现在再来看看,系统为我们生成的java文件:

/*
 * This file is auto-generated.  DO NOT MODIFY.
 * Original file: /Users/luojun/Desktop/ForJianShu/app/src/main/aidl/com/xiasuhuei321/forjianshu/CalcManage.aidl
 */
package com.xiasuhuei321.forjianshu;

// Declare any non-default types here with import statements
// 继承了IInterface,同时自己也是也是一个接口,所有可以再Binder中传输的接口都需要继承IIterface
public interface CalcManage extends android.os.IInterface {
    /**
     * Local-side IPC implementation stub class.
     */
    // Binder类,当客户端和服务端都位于同一个进程的时候,方法调用不会走跨进程的transact过程,
    // 而当两者位于不同进程时,方法调用需要走transact过程。逻辑由Stub的内部代理类Proxy来完成
    public static abstract class Stub extends android.os.Binder implements com.xiasuhuei321.forjianshu.CalcManage {
        // Binder的唯一标识,一般用当前Binder的类名标识
        private static final java.lang.String DESCRIPTOR = "com.xiasuhuei321.forjianshu.CalcManage";

        /**
         * Construct the stub at attach it to the interface.
         */
        public Stub() {
            this.attachInterface(this, DESCRIPTOR);
        }

        /**
         * Cast an IBinder object into an com.xiasuhuei321.forjianshu.CalcManage interface,
         * generating a proxy if needed.
         */
        // 用于将服务端的Binder对象转换成客户端所需的AIDL接口类型的对象,这种转换过程是区分进程的,如果
        // 客户端和服务端位于同一进程,那么此方法返回的就是服务端的Stub对象本身,否则返回的就是系统
        // 封装后的Stub.proxy
        public static com.xiasuhuei321.forjianshu.CalcManage asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin != null) && (iin instanceof com.xiasuhuei321.forjianshu.CalcManage))) {
                return ((com.xiasuhuei321.forjianshu.CalcManage) iin);
            }
            return new com.xiasuhuei321.forjianshu.CalcManage.Stub.Proxy(obj);
        }

        // 返回当前Binder对象
        @Override
        public android.os.IBinder asBinder() {
            return this;
        }

        // 这个方法运行在客户端,当客户端远程调用此方法时,它的内部实现是这样的:创建该方法所需要的输入类型
        // Parcel对象_data、输出型Parcel对象_reply和返回值对象List;然后把该方法的参数信息写入_data中
        // (如果有参数的话);接着调用transact方法来发起RPC(远程过程调用)请求,同事当前线程挂起;然后
        // 服务端的onTransact方法会被调用,直到RPC过程返回后,当前线程继续执行,并从_reply中取出RPC过程
        // 的返回结果;返回_reply中的数据。
        @Override
        public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
            switch (code) {// 唯一标识,区分执行哪个方法
                case INTERFACE_TRANSACTION: {
                    reply.writeString(DESCRIPTOR);
                    return true;
                }
                case TRANSACTION_plus: {
                    // 与客户端的writeInterfaceToken对用,标识远程服务的名称
                    data.enforceInterface(DESCRIPTOR);
                    // 读取客户端传入的两个参数
                    int _arg0;
                    _arg0 = data.readInt();
                    int _arg1;
                    _arg1 = data.readInt();
                    // 这里涉及到java多态,是一个基本的概念,这里最终调用的是我们在
                    // Service中创建的实现类中的方法。
                    int _result = this.plus(_arg0, _arg1);
                    reply.writeNoException();
                    reply.writeInt(_result);
                    return true;
                }
                case TRANSACTION_min: {
                    data.enforceInterface(DESCRIPTOR);
                    int _arg0;
                    _arg0 = data.readInt();
                    int _arg1;
                    _arg1 = data.readInt();
                    int _result = this.min(_arg0, _arg1);
                    reply.writeNoException();
                    reply.writeInt(_result);
                    return true;
                }
            }
            return super.onTransact(code, data, reply, flags);
        }

        private static class Proxy implements com.xiasuhuei321.forjianshu.CalcManage {
            private android.os.IBinder mRemote;

            Proxy(android.os.IBinder remote) {
                mRemote = remote;
            }

            @Override
            public android.os.IBinder asBinder() {
                return mRemote;
            }

            public java.lang.String getInterfaceDescriptor() {
                return DESCRIPTOR;
            }

            // CalcManage.aidl中声明的方法。这个方法运行在客户端,当客户端远程调用此方法时,它的内部
            // 实现是这样的:创建该方法所需要的输入型Parcel对象_data、输出型Parcel对象_reply和返回值
            // 对象
            @Override
            public int plus(int a, int b) throws android.os.RemoteException {
                // 创建输入对象
                android.os.Parcel _data = android.os.Parcel.obtain();
                // 创建输出对象
                android.os.Parcel _reply = android.os.Parcel.obtain();
                // 返回值
                int _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    // 将参数写入_data
                    _data.writeInt(a);
                    _data.writeInt(b);
                    // 发起RPC(远程郭恒调用)请求,当前线程挂起。服务端的onTransact方法会被
                    // 调用,直到RPC过程返回后,当前线程继续执行,并从_reply中取出RPC过程中的
                    // 返回结果;
                    mRemote.transact(Stub.TRANSACTION_plus, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.readInt();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }

            @Override
            public int min(int a, int b) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                int _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeInt(a);
                    _data.writeInt(b);
                    mRemote.transact(Stub.TRANSACTION_min, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.readInt();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }
        }

        // 用于标识plus和min方法
        static final int TRANSACTION_plus = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
        static final int TRANSACTION_min = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
    }

    public int plus(int a, int b) throws android.os.RemoteException;

    public int min(int a, int b) throws android.os.RemoteException;
}

写到这,我觉得有必要再重新审视一下Binder了,因为通过以上的代码和注释,你一定发现老提到Binder。Binder是Android中的一个类,它实现了IBinder接口。从IPC(Inter-Process Communication,进程间通信)角度来说,Binder是Android中的一种跨进程通信方式。从Android应用层来说,Binder是客户端和服务端调用的Binder对象,通过这个Binder对象,客户端就可以获取服务端提供的服务或者数据,这里的服务端包括普通服务和基于AIDL的服务。

本来还想分个服务端,客户端继续吹吹牛,但是写着写着觉得我注释够详细了。。。不吹了不吹了,各位自己看上面我带注释的代码吧。

写到这感觉篇幅有点小长了,关于Binder、Messenger和IPC还是留待下一篇吧~

下一篇

参考资料

  • Android 5.0之后隐式声明Intent 启动Service引发的问题

  • Android aidl Binder框架浅析

  • Android API指南

  • Android开发艺术探索

  • Android:学习AIDL,这一篇文章就够了

你可能感兴趣的:(Android-Service入门到崩溃)