AIDL学习(一)---IPC通讯


  • 为毛推AIDL

AIDL是用来干啥的?为毛要有AIDL?之前听得最多的就是实现跨进程访问其他应用程序,和其他应用程序通讯。但是有没有想过,很多技术都可以实现访问,BroadcastReceiver,ContentProvider,Messenger都可以,没错吧,是不是有点尴尬啊,那究竟为毛呢?这个问题,那就只能去找google了,看文档原话:

Note: Using AIDL is necessary only if you allow clients from different applications to access your service for IPC and want to handle multithreading in your service. 
If you do not need to perform concurrent IPC across different applications, 
you should create your interface by implementing a Binder or, 
if you want to perform IPC, but do not need to handle multithreading, implement your interface using a Messenger.

Regardless, be sure that you understand Bound Services before implementing an AIDL.

重点在第一句:“只有当你允许来自不同的客户端访问你的服务并且需要处理多线程问题时你才必须使用AIDL”。可见AIDL是处理多线程、多客户端并发访问的。而Messager是单线程处理。

  • AIDL简单语法介绍
    AIDL 使用简单语法,使您能通过可带参数和返回值的一个或多个方法来声明接口。 参数和返回值可以是任意类型,甚至可以是其他 AIDL 生成的接口。

    您必须使用 Java 编程语言构建 .aidl 文件。每个 .aidl 文件都必须定义单个接口,并且只需包含接口声明和方法签名。

默认情况下,AIDL 支持下列数据类型:

  • Java 编程语言中的所有原语类型(如 int、long、char、boolean 等等)

  • String

  • CharSequence

  • List
    List 中的所有元素都必须是以上列表中支持的数据类型、其他 AIDL 生成的接口或您声明的可打包类型。 可选择将 List 用作“通用”类(例如,List)。另一端实际接收的具体类始终是 ArrayList,但生成的方法使用的是 List 接口。

  • Map
    Map 中的所有元素都必须是以上列表中支持的数据类型、其他 AIDL 生成的接口或您声明的可打包类型。 另一端实际接收的具体类始终是 HashMap,但生成的方法使用的是 Map 接口,不支持通用Map(如:

    Map<String,Integer> 形式的map)
  • 您必须为以上未列出的每个附加类型加入一个 import 语句,即使这些类型是在与您的接口相同的软件包中定义,换句话说,要自己引入包名。

  • 注意:(1). aidl文件中定义的方法可带零个或多个参数,返回值或空值;
    (2). 所有非原语参数都需要指示数据走向的方向标记。可以是 in、out 或 inout(见以下示例).默认是in,这里的方向标记不要乱标,只标真正需要的方向,因为编组参数开销极大。
    (3). .aidl 文件中包括的所有代码注释都包含在生成的 IBinder 接口中(import 和 package 语句之前的注释除外)
    (4). 只支持方法;您不能公开 AIDL 中的静态字段。
    (5).oneway关键字:看一下文档的叙述

The oneway keyword modifies the behavior of remote calls. When used, a remote call does not block; it simply sends the transaction data and immediately returns. The implementation of the interface eventually receives this as a regular call from the Binder thread pool as a normal remote call. If oneway is used with a local call, there is no impact and the call is still synchronous.

也就是说,当定义.aidl文件的时候interface前使用oneway关键字,能够非阻塞的调用远程方法。
这里可以对比一下两种情况瞎生成的.java文件:

不声明 oneway 时,mRemote.transact 传入的最后一个参数是 0;声明 oneway 时,mRemote.transact 传入的最后一个参数是 android.os.IBinder.FLAG_ONEWAY 。

  • 定义AIDL接口步骤
    1. 创建 .aidl文件
      此文件定义带有方法签名的编程接口。
    2. 实现接口
      Android SDK 工具基于您的 .aidl 文件,使用 Java 编程语言生成一个接口。此接口具有一个名为 Stub 的内部抽象类,用于扩展 Binder 类并实现 AIDL 接口中的方法。您必须扩展 Stub 类并实现方法。
    3. 向客户端公开该接口
      实现 Service 并重写 onBind()以返回 Stub 类的实现。

那就按照上面的步骤逐步实现,先按照官方doc来个简单的demo,先能通讯再说。

//1,创建.aidl文件

/**
(1)如果是eclipse,在项目的 gen/ 目录中生成 IBinder 接口文件,与.aidl同名的.java文件
(2)使用 Android Studio,增量编译几乎会立即生成 Binder 类。
*/

//这里的包名,要自己手动导入
package com.charles_lun.db.aidl;

interface IRemoteService{

    int getPid();
    //基本数据类型参数方向默认是in
    void basicTypes(int anInt,long aLong,boolean aBoolean,
                    float aFloat,double aDouble,String aString);

}

//2,实现接口
//3,向客户端公开接口
public class AIDLService extends Service {

    @Override
    public void onCreate() {
        super.onCreate();
        System.out.println("on create....");
    }

    //向客户端公开接口
    @Override
    public IBinder onBind(Intent intent) {
        System.out.println("on bind....");
        return mBinder;
    }



    @Override
    public boolean onUnbind(Intent intent) {
        System.out.println("on un bind...............");
        return super.onUnbind(intent);
    }





    //接口的实现
    private final IRemoteService.Stub mBinder = new IRemoteService.Stub() {

        @Override
        public int getPid() throws RemoteException {
            System.out.println("get pid Thread: " + Thread.currentThread().getName());
            System.out.println("DDService getPid ");
            return Process.myPid();
        }

        @Override
        public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString)
                throws RemoteException {
            System.out.println("basic types Thread: " + Thread.currentThread().getName());
            System.out.println("basicTypes aDouble: " + aDouble + " anInt: " + anInt + " aBoolean " + aBoolean
                    + " aString " + aString);

        }

    };

}

服务在Manifest.xml中声明:

 <service 
             android:name=".aidl.AIDLService" 
             [!--允许其它应用访问--]
             android:exported="true"
             [!--声明service运行在单独的进程中--]
             android:process=":remote"
             >
             <intent-filter>
                [!--添加action,其它应用隐式访问--]
                 <action android:name="com.charles_lun.db.aidl.IRemoteService"/>
                 <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </service>

在实现 AIDL 接口时应注意遵守以下这几个规则:

  1. 由于不能保证在主线程上执行传入调用,因此您一开始就需要做好多线程处理准备,并将您的服务正确地编译为线程安全服务

  2. 默认情况下,IPC调用是同步调用
    这里一定要注意:如果您明知服务完成请求的时间不止几毫秒,就不应该从 Activity 的主线程调用服务,因为这样做可能会使应用挂起(Android
    可能会显示“Application is Not Responding”对话框)—
    您通常应该从客户端内的单独线程调用服务。您引发的任何异常都不会回传给调用方

同一个应用中通讯,注意这里我把Service声明到了一个单独的进程中,所以即使同一应用也是跨进程的

public class AIDLActivity extends Activity {
    IRemoteService binder;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Intent intent = new Intent(this,AIDLService.class);
        bindService(intent, connection, BIND_AUTO_CREATE);

        findViewById(R.id.btn).setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                try {
                    if(binder == null){
                        return;
                    }
                    int pid = binder.getPid();
                    System.out.println("pid:"+pid);
                    binder.basicTypes(10, 15L, false, 5.5f, 6.5d, "你大爷");
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        });

    }



    @Override
    protected void onDestroy() {
        unbindService(connection);
        super.onDestroy();
        System.out.println("on destory.....");
    }




    ServiceConnection connection = new ServiceConnection() {

        @Override
        public void onServiceDisconnected(ComponentName name) {
            System.out.println("disconnect....thread:"+Thread.currentThread().getName());
            binder = null;
        }

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            System.out.println("connect.....thread:"+Thread.currentThread().getName());
            //这里不要搞错了
            binder = IRemoteService.Stub.asInterface(service);
        }
    };

}

如果不相信我说的,看图:
process.png-7.5kB
不是同一个进程吧。
看一下打印结果:

12-01 17:11:24.562 31350-31350/? I/System.out: on create....
12-01 17:11:24.562 31350-31350/? I/System.out: on bind....
12-01 17:11:24.581 31326-31326/? I/System.out: connect.....thread:main
12-01 17:11:30.645 31350-31371/? I/System.out: get pid Thread: Binder_1
12-01 17:11:30.645 31350-31371/? I/System.out: DDService getPid 
12-01 17:11:30.645 31326-31326/? I/System.out: pid:31350
12-01 17:11:30.646 31350-31372/? I/System.out: basic types Thread: Binder_2
12-01 17:11:30.646 31350-31372/? I/System.out: basicTypes aDouble: 6.5 anInt: 10 aBoolean false aString 你大爷
12-01 17:11:39.946 31326-31326/? I/System.out: on destory.....
12-01 17:11:39.947 31350-31350/? I/System.out: on un bind...............

这里一定要注意:
1,onServiceConnected 原谅我的无知,我打印之后才知道,这里一直运行在UIThread,而不是之前以为的运行在发起绑定服务的同一线程内
2,getPid(),basicTypes(),运行在不同线程内
3,并没有回调onServiceDisconnected

再来搞一下,不同的应用调用

//1,新建跟.aidl中包名一致的包,将.aidl文件copy过来放在此包中
//2,绑定服务开始访问

//看这里,这是另外一个工程
package com.example.animationdemo;
//看这里包名
import com.charles_lun.db.aidl.IRemoteService;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.view.View;
import android.view.View.OnClickListener;

public class AIDLAcitivity extends Activity {

    IRemoteService binder;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Intent intent = new Intent("com.charles_lun.db.aidl.IRemoteService");
        bindService(intent, con, BIND_AUTO_CREATE);
        findViewById(R.id.button1).setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                if(binder ==null){
                    return;
                }

                try {
                    int pid = binder.getPid();
                    System.out.println("pid:"+pid);
                    binder.basicTypes(1, 1l, true, 1.0f, 1d, "1");
                } catch (RemoteException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }

            }
        });

    }

    ServiceConnection con = new ServiceConnection() {

        @Override
        public void onServiceDisconnected(ComponentName name) {


        }

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
                binder = IRemoteService.Stub.asInterface(service);
        }
    };

    @Override
    protected void onDestroy() {
        unbindService(con);
        super.onDestroy();
    }

}

先看下进程,省的说我骗你
process.png-9kB
再来看一下打印:

12-01 17:27:04.634 31350-31350/? I/System.out: on create....
12-01 17:27:04.634 31350-31350/? I/System.out: on bind....
12-01 17:28:56.089 31350-31371/? I/System.out: get pid Thread: Binder_1
12-01 17:28:56.090 31350-31371/? I/System.out: DDService getPid 
12-01 17:28:56.092 3518-3518/? I/System.out: pid:31350
12-01 17:28:56.093 31350-31372/? I/System.out: basic types Thread: Binder_2
12-01 17:28:56.093 31350-31372/? I/System.out: basicTypes aDouble: 1.0 anInt: 1 aBoolean true aString 1
12-01 17:29:00.811 31350-31350/? I/System.out: on un bind...............

没错吧,链接的是remote那个进程。
注意:每次unbind之后,重新绑定服务,都会重新onCreate();

  • 再来看一下通过IPC传递对象
    看一下Google Doc的介绍,通过 IPC 接口把某个类从一个进程发送到另一个进程是可以实现的。 不过,您必须确保该类的代码对 IPC 通道的另一端可用,并且该类必须支持 Parcelable 接口。支持 Parcelable 接口很重要,因为 Android 系统可通过它将对象分解成可编组到各进程的原语。(原谅我是个英语渣),换句话说,你要实现Parcelable接口。

    实现Parcelable接口步骤(已经好久不写了,直接as生成,大部分人是不是跟我一样):

    1. 让您的类实现 Parcelable 接口。
    2. 实现 writeToParcel,它会获取对象的当前状态并将其写入 Parcel。
    3. 为您的类添加一个名为 CREATOR 的静态字段,这个字段是一个实现 Parcelable.Creator 接口的对象。
    4. 最后,创建一个声明可打包类的 .aidl 文件(按照下文 Rect.aidl 文件所示步骤)。
      如果您使用的是自定义编译进程,切勿在您的编译中添加 .aidl 文件。 此 .aidl 文件与 C 语言中的头文件类似,并未编译。

看一下创建Demo:

import android.os.Parcel;
import android.os.Parcelable;

public final class Rect implements Parcelable {
    public int left;
    public int top;
    public int right;
    public int bottom;

    public static final Parcelable.Creator<Rect> CREATOR = new
Parcelable.Creator<Rect>() {
        public Rect createFromParcel(Parcel in) {
            return new Rect(in);
        }

        public Rect[] newArray(int size) {
            return new Rect[size];
        }
    };

    public Rect() {
    }

    private Rect(Parcel in) {
        readFromParcel(in);
    }

    public void writeToParcel(Parcel out) {
        out.writeInt(left);
        out.writeInt(top);
        out.writeInt(right);
        out.writeInt(bottom);
    }

    public void readFromParcel(Parcel in) {
        left = in.readInt();
        top = in.readInt();
        right = in.readInt();
        bottom = in.readInt();
    }
}
  • 总结一下调用IPC步骤

    1. 在项目 src/ 目录中加入 .aidl 文件。
    2. 声明一个 IBinder 接口实例(基于 AIDL 生成)。
    3. 实现 ServiceConnection。
    4. 调用 {@linkandroid.content.Context#bindService(android.content.Intent,android.content.ServiceConnection,int) Context.bindService()},以传入您的 ServiceConnection 实现。
    5. 在您的 onServiceConnected() 实现中,您将收到一个 IBinder 实例(名为 service)。调用 YourInterfaceName.Stub.asInterface((IBinder)service),以将返回的参数转换为 YourInterface 类型。
    6. 调用您在接口上定义的方法。您应该始终捕获 DeadObjectException 异常,它们是在连接中断时引发的;这将是远程方法引发的唯一异常。
    7. 如需断开连接,请使用您的接口实例调用 {@linkandroid.content.Context#unbindService(android.content.ServiceConnection) Context.unbindService()}。
      注意:对象是跨进程计数的引用。您可以将匿名对象作为方法参数发送。

下面贴上一个复杂点的Demo,涉及到2个方面的内容:
1. 服务权限的控制:
这个我只列出2种方案,
(1)自定义权限,在Service#onBind(Intent intent)中判断,调用服务的应用是否有这个权限,没有就返回null
(2)在Stub#onTransact()中判断,检查权限,检查包名,
2. 在客户端在服务端注册监听,取消监听

//1,定义.aidl文件

//BookManager.aidl
//手动导包
package com.example.aidl;

import com.example.aidl.Book;
import com.example.aidl.IOnNewBookArrivedListener;

interface BookManager{

List<Book> getBooks();
//除了基本类型之外,都要标明实际用到的方向
void addBook(in Book book);

void registerListener(IOnNewBookArrivedListener listener);
void unregisterListener(IOnNewBookArrivedListener listener);

}

//BookManager.aidl用到了Book,IOnNewBookArrivedListener,所以还要新建2个.aidl文件

//Book.aidl
package com.example.aidl;
//小写
parcelable Book;

//IOnNewBookArrivedListener.aidl
package com.example.aidl;
import com.example.aidl.Book;

interface IOnNewBookArrivedListener {
    void onNewBookArrived(in Book book);
}

//2,BookManager.Stub子类继承
//3,暴露给客户端
public class BookService extends Service {
    private List<Book> books = new ArrayList<Book>();
    private AtomicBoolean isServiceDestory = new AtomicBoolean();

    RemoteCallbackList<IOnNewBookArrivedListener> listeners = new RemoteCallbackList<IOnNewBookArrivedListener>();

    // 暴露给客户端
    @Override
    public IBinder onBind(Intent intent) {
        System.out.println("on bind....");
        return binder;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        isServiceDestory.set(false);
        System.out.println("on create....");
        Book book = new Book();
        book.setName("android ...");
        book.setPrice(100);
        books.add(book);
        //开启线程,每5s中新书加入
        new Thread(new WorkThread()).start();
    }

    @Override
    public boolean onUnbind(Intent intent) {
        System.out.println("on unbind.....");
        return super.onUnbind(intent);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        isServiceDestory.set(true);
        listeners.kill();
    }

    // 继承interface.stub
    private final BookManager.Stub binder = new BookManager.Stub() {
        //权限
        // 验证权限和包名,符合要求,才能用我服务中的方法
        @Override
        public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
            System.out.println("on transact.....");
            int permission = checkCallingOrSelfPermission("com.example.aidl.BOOK_ACCESS_SERVICE");
            if (permission == PackageManager.PERMISSION_DENIED) {
                return false;
            }
            String packageName = null;
            String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
            if (packages != null && packages.length > 0) {
                packageName = packages[0];
            }
            //包名也要符合
            if (!packageName.startsWith("com")) {
                return false;
            }
            return super.onTransact(code, data, reply, flags);
        }

        @Override
        public List<Book> getBooks() throws RemoteException {
            System.out.println("get books thread:" + Thread.currentThread().getName());
            synchronized (this) {
                if (books == null) {
                    books = new ArrayList<Book>();
                    return books;
                }
                return books;
            }
        }

        @Override
        public void addBook(Book book) throws RemoteException {
            synchronized (this) {
                if (books == null) {
                    books = new ArrayList<Book>();
                }
                books.add(book);
                System.out.println("add book...." + books.toString());
            }

        }

        @SuppressLint("NewApi")
        @Override
        public void registerListener(IOnNewBookArrivedListener listener) throws RemoteException {
            //注意RemoteCallbackList 不熟悉的去百度一下
            //注册监听,客户端可以收到新书通知
            listeners.register(listener);
            System.out.println("registerListener listeners size:"+listeners.beginBroadcast());
            listeners.finishBroadcast();
        }

        @SuppressLint("NewApi")
        @Override
        public void unregisterListener(IOnNewBookArrivedListener listener) throws RemoteException {
            //解除监听,收不到新书通知
            listeners.unregister(listener);
            System.out.println("unregisterListener listeners size:"+listeners.beginBroadcast());
            listeners.finishBroadcast();
        }
    };

    void onNewBookArrived(Book book) {
        try {
            books.add(book);
            int n = listeners.beginBroadcast();
            //通知所有的客户端,新书来了
            for (int i = 0; i < n; i++) {
                IOnNewBookArrivedListener listener = listeners.getBroadcastItem(i);
                if(listener != null){
                    listener.onNewBookArrived(book);
                }
            }
            listeners.finishBroadcast();
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

//工作线程
    private class WorkThread implements Runnable {

        @Override
        public void run() {

            while (!isServiceDestory.get()) {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                int price = books.size() + 1;
                Book book = new Book();
                book.setPrice(price);
                book.setName("book:" + price);
                onNewBookArrived(book);
            }
        }

    }

}

Manifest.xml声明权限

     <!-- 自定义权限 -->
<permission android:name="com.example.aidl.BOOK_ACCESS_SERVICE" android:protectionLevel="normal"/>

<uses-permission android:name="com.example.aidl.BOOK_ACCESS_SERVICE"/>

客户端调用

public class BookActivity extends Activity {

    BookManager manager;
    int i=0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.button).setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                Toast.makeText(BookActivity.this, "add book", 1500).show();
                if(!isBind()){
                    attemptBind();
                    return;
                }

                Book book = new Book();
                book.setName("我好帅"+i);
                book.setPrice(i++);
                try {
                    manager.addBook(book);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        });

        findViewById(R.id.button2).setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                if(manager ==null){
                    System.out.println("null....");
                    return;
                }

                try {
                    System.out.println("main get booksL"+manager.getBooks().toString());
                } catch (RemoteException e) {
                    e.printStackTrace();
                }

            }
        });
    }




    @Override
    protected void onStart() {
        super.onStart();
        if(!isBind()){
            attemptBind();
        }
    }


    private void attemptBind(){
        Intent intent = new Intent("com.example.aidl.BookService");
        intent.setPackage("com.example.aidl");
        bindService(intent, con, BIND_AUTO_CREATE);
    }



    @Override
    protected void onStop() {
        super.onStop();
    }

    private boolean isBind(){
        if(manager != null&& manager.asBinder().isBinderAlive()){
            return true;
        }
        return false;
    }




    @Override
    protected void onDestroy() {
        super.onDestroy();
        if(isBind()){
            try {
                manager.unregisterListener(listener);
            } catch (RemoteException e) {
                e.printStackTrace();
                System.out.println(e.toString());
            }
        }
        unbindService(con);
    }


    ServiceConnection con = new ServiceConnection() {

        @Override
        public void onServiceDisconnected(ComponentName name) {
            System.out.println("on disconneted....");
            manager = null;
        }

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            System.out.println("on connected....thread:"+Thread.currentThread().getName());
            manager = BookManager.Stub.asInterface(service);
            System.out.println("is manager null:"+(manager==null));
            try {
                manager.registerListener(listener);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    };

    private final IOnNewBookArrivedListener.Stub listener = new IOnNewBookArrivedListener.Stub() {

        //这是运行在非UIThread,不信,自己打印线程
        //如果要根据这个结果更新UI,这里就要你手动切换到UIThread,至于选择什么方案,随便你
        @Override
        public void onNewBookArrived(Book book) throws RemoteException {
            System.out.println("new book:"+book.toString()+",thread:"+Thread.currentThread().getName());
        }
    };

}

再看一下,打印结果:

12-02 00:04:19.936  10706-10706/? I/System.out﹕ on create....
12-02 00:04:19.940  10706-10706/? I/System.out﹕ on bind....
12-02 00:04:19.940  10674-10674/com.example.aidl I/System.out﹕ on connected....thread:main
12-02 00:04:19.940  10674-10674/com.example.aidl I/System.out﹕ is manager null:false
12-02 00:04:19.940  10706-10736/? I/System.out﹕ on transact.....
12-02 00:04:19.940  10706-10736/? I/System.out﹕ registerListener listeners size:1
12-02 00:04:24.940  10674-10705/com.example.aidl I/System.out﹕ new book:name:book:2,price:2,thread:Binder_2
12-02 00:04:29.944  10674-10704/com.example.aidl I/System.out﹕ new book:name:book:3,price:3,thread:Binder_1
12-02 00:04:34.948  10674-10705/com.example.aidl I/System.out﹕ new book:name:book:4,price:4,thread:Binder_2
12-02 00:04:39.948  10674-10704/com.example.aidl I/System.out﹕ new book:name:book:5,price:5,thread:Binder_1
12-02 00:04:44.736  10706-10736/com.example.aidl:remote I/System.out﹕ on transact.....
12-02 00:04:44.736  10706-10736/com.example.aidl:remote I/System.out﹕ unregisterListener listeners size:0
12-02 00:04:44.740  10706-10706/com.example.aidl:remote I/System.out﹕ on unbind.....

不论是权限,还是监听都符合预期,说明完美,关于AIDL的使用,暂时我也就知道这么多,鉴于水平有限,如果有错误的地方,请大大门指正。

参考:
Google AIDL
《Android开发艺术探索》

猜你喜欢

转载自blog.csdn.net/baidu_17508977/article/details/53428771