IPC是指Android中的进程间通信,即在不同进程之间传递消息数据,Android中可实现进程通信的方法有很多,比如Intent、ContentProvider、Messenger、Binder、Socket或是利用文件,这些方式各有千秋,都有最适合使用的场景,这次要介绍的是一种安全高效的面向对象式的IPC实现——Binder。

当使用bindService()绑定一个服务时,service会在其onBind()方法中返回一个Binder对象,然后在client的ServiceConnection中获取这个Binder,即可跨进程使用service的方法,接下来我们就来看一看Binder的实现原理。

在Android中,实现Binder很简单,不需要我们去写,只需要写一个aidl文件,在其中写一个接口,声明需要的方法,其他的工作通过编译之后系统会为我们完成,最后生成java文件。Android为我们提供了这这种简单的Binder使用方式,虽然简化了开发,但也一定程度的限制了我们对其工作原理的深入理解,下面就以系统生成的Binder类来讲解一下Binder构造。

本文会分四个个部分来分析Binder:

1.Binder的组成结构
2.Binder的使用方法
3.Binder对象的传递流程
4.Binder对client请求的处理过程


Aidl生成Binder类

首先,创建一个aidl文件,如下:

package com.ipctest.aidl;interface IUser {   boolean login(String userName,String userPwd);   void logout(String userName);}

然后编译工程,在AndroidStudio的目录结构下,生成的.java文件在build–generated–source–aidl–debug目录下,我得到的文件经过格式整理如下:

/* * This file is auto-generated.  DO NOT MODIFY. * Original file:  */package com.ipctest.aidl;// Declare any non-default types here with import statementspublic interface IUser extends android.os.IInterface{    /** Local-side IPC implementation stub class. */    public static abstract class Stub extends android.os.Binder implements com.ipctest.aidl.IUser{        private static final java.lang.String DESCRIPTOR = "com.ipctest.aidl.IUser";        /** Construct the stub at attach it to the interface. */        public Stub(){            this.attachInterface(this, DESCRIPTOR);        }        /**         * Cast an IBinder object into an com.ipctest.aidl.IUser interface,         * generating a proxy if needed.         */        public static com.ipctest.aidl.IUser asInterface(android.os.IBinder obj)        {            if ((obj==null)) {                return null;            }            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);            if (((iin!=null)&&(iin instanceof com.ipctest.aidl.IUser))) {                return ((com.ipctest.aidl.IUser)iin);            }            return new com.ipctest.aidl.IUser.Stub.Proxy(obj);        }        @Override         public android.os.IBinder asBinder()        {            return this;        }        @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_login:                {                    data.enforceInterface(DESCRIPTOR);                    java.lang.String _arg0;                    _arg0 = data.readString();                    java.lang.String _arg1;                    _arg1 = data.readString();                    boolean _result = this.login(_arg0, _arg1);                    reply.writeNoException();                    reply.writeInt(((_result)?(1):(0)));                    return true;                }                case TRANSACTION_logout:                {                    data.enforceInterface(DESCRIPTOR);                    java.lang.String _arg0;                    _arg0 = data.readString();                    this.logout(_arg0);                    reply.writeNoException();                    return true;                }            }            return super.onTransact(code, data, reply, flags);        }        private static class Proxy implements com.ipctest.aidl.IUser{            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;            }            @Override             public boolean login(java.lang.String userName, java.lang.String userPwd) throws android.os.RemoteException            {                android.os.Parcel _data = android.os.Parcel.obtain();                android.os.Parcel _reply = android.os.Parcel.obtain();                boolean _result;                try {                    _data.writeInterfaceToken(DESCRIPTOR);                    _data.writeString(userName);                    _data.writeString(userPwd);                    mRemote.transact(Stub.TRANSACTION_login, _data, _reply, 0);                    _reply.readException();                    _result = (0!=_reply.readInt());                }finally {                    _reply.recycle();                    _data.recycle();                }                return _result;            }            @Override             public void logout(java.lang.String userName) throws android.os.RemoteException{                android.os.Parcel _data = android.os.Parcel.obtain();                android.os.Parcel _reply = android.os.Parcel.obtain();                try {                    _data.writeInterfaceToken(DESCRIPTOR);                    _data.writeString(userName);                    mRemote.transact(Stub.TRANSACTION_logout, _data, _reply, 0);                    _reply.readException();                }finally {                    _reply.recycle();                    _data.recycle();                }            }        }    static final int TRANSACTION_login = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);    static final int TRANSACTION_logout = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);    }    public boolean login(java.lang.String userName, java.lang.String userPwd) throws android.os.RemoteException;    public void logout(java.lang.String userName) throws android.os.RemoteException;}

Binder类结构

上面就是是系统生成的IUser.java,可能看起来会有些头疼,但结构其实非常简单,上面作为源码参考,下面我将其方法内容省略,将结构分离出来,这个接口继承于IInterface,在其中中实现了一个内部类Stub,Stub又有一个内部类Proxy,代码如下:

package com.ipctest.aidl;//AIDL文件中定义的IUser接口public interface IUser extends android.os.IInterface{    //IUser中的内部类,继承于Binder类并实现了IUser接口,在service中传递的就是这个类    public static abstract class Stub extends android.os.Binder implements com.ipctest.aidl.IUser{        //接口的唯一标识,一般由包名+类名组成        private static final java.lang.String DESCRIPTOR = "com.ipctest.aidl.IUser";        public Stub(){            //在构造方法中将自身接口标识存储起来            this.attachInterface(this, DESCRIPTOR);        }        public static com.ipctest.aidl.IUser asInterface(android.os.IBinder obj)        {            //获取binder对象            //在这里会调用obj.queryLocalInterface(DESCRIPTOR)来获取binder,在service返回Binder时会判断client请求进行处理            //如果请求来自当前进程,queryLocalInterface()会返直接返回构造方法中attachInterface()的binder对象,也就是binder本身            //如果来自其他进程,queryLocalInterface方法直接返回null            //这时就需要创建一个Proxy对象(Stub的内部代理类)供client使用        }        @Override         public android.os.IBinder asBinder()        {            //获取当前Binder            return this;        }        @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException{            //在跨进程请求时被调用,请求先被Proxy对象处理,Proxy将方法参数序列化之后            //和方法编号以及方法参数和用于存储返回值的序列化对象(如果有返回值)一同交给此方法            //然后在这里将参数还原,并调用相应的方法进行处理,最后将返回值序列化后返回给Proxy。        }        //Stub的内部类,当请求来自同一进程时,不会使用,当请求来自另一个进程时,会将client得到的binder包装成它的实例        private static class Proxy implements com.ipctest.aidl.IUser{            private android.os.IBinder mRemote;            //持有一个IBinder,通常就是stub本身            Proxy(android.os.IBinder remote){                mRemote = remote;            }            //获取当前的Proxy对象            @Override             public android.os.IBinder asBinder(){                return mRemote;            }            public java.lang.String getInterfaceDescriptor(){                return DESCRIPTOR;            }            @Override             public boolean login(java.lang.String userName, java.lang.String userPwd) throws android.os.RemoteException{                //login方法请求封装            }            @Override             public void logout(java.lang.String userName) throws android.os.RemoteException{                //logout方法请求封装            }        }        //IUser借口中两个方法的code    static final int TRANSACTION_login = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);    static final int TRANSACTION_logout = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);    }    //待实现的方法    public boolean login(java.lang.String userName, java.lang.String userPwd) throws android.os.RemoteException;    public void logout(java.lang.String userName) throws android.os.RemoteException;}

Binder使用方法

上面就是IUser的结构,下面通过一个例子先演示一下Binder的实现方式:

//首先创建Service,还是使用前面的aidl生成的Binderpublic class MyService extends Service{    private final String TAG="BinderTest";    //创建Binder对象,实现两个方法    private Binder mBinder= new IUser.Stub() {        @Override        public boolean login(String userName, String userPwd) throws RemoteException {            Log.d(TAG,userName+"   登录成功!");            return true;        }        @Override        public void logout(String userName) throws RemoteException {            Log.d(TAG,userName+"   退出成功!");        }    };    //在onBind中返回Binder对象    @Override    public IBinder onBind(Intent intent) {        return mBinder;    }}
//然后在Activity中启动服务public class MainActivity extends AppCompatActivity {    private final String TAG = "MyServiceTest";    private EditText mUserNameEdt, mUserPwdEdt;    private Button mLoginBtn, mLogoutBtn;    IUser mUserBinder;    //创建ServicerConnection    ServiceConnection mServiceConnection = new ServiceConnection() {        @Override        public void onServiceConnected(ComponentName name, IBinder service) {            Log.d(TAG, "onServiceConnected");            mUserBinder = IUser.Stub.asInterface(service);        }        @Override        public void onServiceDisconnected(ComponentName name) {        }    };    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        Log.d(TAG, "oncreate");        setContentView(R.layout.activity_main);        initLayout();        //绑定服务        Intent intent = new Intent(MainActivity.this, MyService.class);        bindService(intent,mServiceConnection,BIND_AUTO_CREATE);    }    private void initLayout() {        mUserNameEdt = (EditText) this.findViewById(R.id.user_name_edt);        mUserPwdEdt = (EditText) this.findViewById(R.id.user_pwd_edt);        mLoginBtn = (Button) this.findViewById(R.id.login_btn);        mLogoutBtn = (Button) this.findViewById(R.id.logout_btn);        //点击登录按钮访问Service的Binder的login方法        mLoginBtn.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                String userName = mUserNameEdt.getText().toString();                String userPwd = mUserPwdEdt.getText().toString();                if (mUserBinder != null) {                    try {                        mUserBinder.login(userName, userPwd);                    } catch (RemoteException e) {                        e.printStackTrace();                    }                }            }        });        //点击退出按钮访问Service的Binder的logout方法        mLogoutBtn.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                String userName = mUserNameEdt.getText().toString();                if (mUserBinder != null) {                    try {                        mUserBinder.logout(userName);                    } catch (RemoteException e) {                        e.printStackTrace();                    }                }            }        });    }

点击按钮结果:


可以发现Binder中的方法被执行了,这是同一进程的运行结果,如果将service运行在独立进程中又会如何呢?

    //在Manifest中为MyService设置一个进程    ".MyService"            android:process=":remote" />

运行结果:

可以看到,在将MyService的进程中login()和logout()被执行了,也就是说点击按钮后成功的跨进程调用了MyService的方法


Binder对象的传递过程

接下来详细解析Binder从Service到client的传递过程

首先看上边的Demo,在MyService中实现了对象

private Binder mBinder= new IUser.Stub() {        @Override        public boolean login(String userName, String userPwd) throws RemoteException {            Log.d(TAG,userName+"   登录成功!");            return true;        }        @Override        public void logout(String userName) throws RemoteException {            Log.d(TAG,userName+"   退出成功!");        }    };@Override    public IBinder onBind(Intent intent) {        return mBinder;    }

IUser.Stub,在文章开头的源码中可以找到,它是一个抽象类,继承与Binder类,这个对象在service与MyService通信使用的Binder,同时它实现了我们定义的Aidl的IUser接口,也就是说这个Binder拥有了我们的自定义方法(在Stub中只是将IUser接口的方法继承了下来,但并没有实现,直到在我们创建这个实例时手动实现了方法),然后通过Binder的onTransact()的code参数将client的请求类型与本地的方法绑定,在将此Binder对象返回给client,单从应用层来看,如此便将方法暴露给了client。(后面注意Stub类是我们自定义的Binder类,后面说的binder对象便是Stub对象)

再看client代码:

ServiceConnection mServiceConnection = new ServiceConnection() {        @Override        public void onServiceConnected(ComponentName name, IBinder service) {            Log.d(TAG, "onServiceConnected");            mUserBinder = IUser.Stub.asInterface(service);        }        @Override        public void onServiceDisconnected(ComponentName name) {        }    }; bindService(intent,mServiceConnection,BIND_AUTO_CREATE);

在client中,创建了一个ServiceConection对象,并在bindService()启动服务时进行绑定,当服务启动ServiceConnection连接成功时:
1、service的onBind()方法被执行,返回我们我们创建的Binder对象(mBinder)
2、clientServiceConection对象的onServiceConnected(ComponentName name, IBinder service)方法被执行,参数service用来接收service返回的Binder,然后在下面这一句代码将得到的Binder转为可识别的对象(asInterface如果是同进程直接返回收到的binder,如果是跨进程会返回一个Binder的内部代理类Proxy的实例),这样client就得到了在Service中创建的Binder,通过aidl的IUser引用即可使用Binder的方法。

mUserBinder = IUser.Stub.asInterface(service);

那么IUser.Stub.asInterface(service),这个方法是到底是如何处理收到的binder对象的呢?来看源码:

public static com.ipctest.aidl.IUser asInterface(android.os.IBinder obj){           /*obj为service的binder对象,先做非空判断*/        if ((obj==null)) {            return null;        }        /*这里验证binder对象中DESCRIPTOR是否合法,是直接返回binder,否返null(详解见下面的源码)*/        android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);        /*如果不为null,将binder对象转换为我们定义的IUser类型,返回给client的ServiceConnection*/        if (((iin!=null)&&(iin instanceof com.ipctest.aidl.IUser))) {            return ((com.ipctest.aidl.IUser)iin);        }        /*如果为null代表为跨进程请求,创建一个Proxy代理对象(Stub的内部类,后面详解)*/        return new com.ipctest.aidl.IUser.Stub.Proxy(obj);    }

上面可以看到asInterface将binder对象进行了处理,如果请求为当前进程,那么同进程可共享内存,即可直接使用service返回的binder对象,但如果请求是跨进程,则将binder对象包装为一个代理对象,返回给client,到这里Binder的传递流程就通了,但如何区分是当前进程还是跨进程呢?关键就在Stub的queryLocalInterface的方法,我们继续深入,这个方法是Stub的父类Binder中的方法,我们看源码:

//在Stub的asInterface中调用了这个方法,DESCRIPTOR为Stub的接口唯一标识,默认为包名路径obj.queryLocalInterface(DESCRIPTOR);//queryLocalInterface源码public IInterface queryLocalInterface(String descriptor) {       //在这里先将传入的接口标识和Binder的字段mDescriptor对比是否一样,是返回mOwner字段,否返回null       //那mDescriptor和mOwner两个字段又代表了什么呢,我们找到其获取值得地方,见下一个方法       if (mDescriptor.equals(descriptor)) {           return mOwner;       }       return null;   }//在看这两个字段的赋值之前,先看看他们的类型,这是这两个字段的声明//可以看到mOwner是一个IInterface 接口引用,也就是说他可以接受任何类型的对象实例private IInterface mOwner;//mDescriptor为一个字符串,然后看赋值private String mDescriptor; //在这里我们发现,attachInterface方法中对mOwner和mDescriptor字段进行了赋值//既如此,那么我们找到attachInterface方法的调用者即可知道这两个字段的内容,看下一个方法public void attachInterface(IInterface owner, String descriptor) {       mOwner = owner;       mDescriptor = descriptor;   }//仔细看了Stub结构的读者应该可以发现,attachInterface方法在Stub的构造方法中就被调用了public Stub(){            //这里传入的参数为this为我们传递的stub对象本身            //DESCRIPTOR为Stub的接口标识,在Stub源码可以看到            this.attachInterface(this, DESCRIPTOR);        }

也就是说在binder对象被创建时,使用attachInterface(this, DESCRIPTOR)将其自身和接口标识存入mOwner和mDescriptor字段
在client接收到这个对象后,调用queryLocalInterface(DESCRIPTOR)方法,将Stub类的DESCRIPTOR字段与mDescriptor比较,如果相同表示client请求来自同一进程,返回mOwner字段,否则表示是跨进程请求,返回null,那么就有一个问题:

Service返回的是同一个Binder对象,且这个对象在构造时就已经为mDescriptor字段赋值,那么为什么在同一进程的client在进行mDescriptor.equals(descriptor)比较的时候是为true成立的,而client在另一个进程时这个条件就为false了呢?

这个就涉及到更底层的知识了,从系统的角度来看,client得到的binder对象引用并不是由service直接交付的,而是通过Binder驱动: 当我们的client需要serivice中binder对象的引用而又不在同一进程时,service首先会将本地内存中binder对象的名字通过处于内核的Binder驱动交给ServiceManagerServiceManager将binder的引用存储起来,在client中通过binder的名字来访问ServiceManager中存储的对binder对象的引用,然后Binder驱动会为client也创建一个Binder对象,不同的是这个对象并不是一个Binder实体,而是对service中binder的方法调用请求的封装(调用通过从ServiceManager中得到的binder引用)

那么到这里就可以知道,之所以mDescriptor.equals(descriptor)在跨进程的时候会不成立,是因为在Binder驱动为client创建binder对象时,这个对象只是一个对service中的binder实体各种业务请求的封装,而不是一个真正的binder实体

想要深入理解这个部分,可以看看:http://blog.csdn.net/universus/article/details/6211589

现在来整理一下:
1、首先在binder对象被创建时,在构造方法中调用attachInterface(this, DESCRIPTOR)将其自身和接口标识存入mOwner和mDescriptor字段
2、Service 的onBinder()返回binder对象,Bidner驱动创建mRemote交给client,client得到binder对象
3、client进行请求方式判断(同进程或跨进程),是同一个进程直接返回binder对象,否则返回代理对象
4、client使用service业务
client拿到binder对象的过程就到这


Binder 请求处理过程

先说service和client在同一进程的情况:同进程内存是可以共享的,所以前面解释过当请求来自同一进程时,client得到的binder就是我们创建的mBinder对象,所以我们调用其方法就是常规的方法调用,

而service和client不在同一进程时,就产生了跨进程的问题,我们知道,不同进程的内存是不可共享的,一个新的进程甚至会导致Application和各个静态变量的重复创建,所以我们就无法直接对binder的方法进行调用,这时就需要通过Binder驱动去访问seriver中的binder。

前面说了,client中使用的binder对象是Binder驱动为client创建的一个“对service中binder的方法调用请求的封装”,那这个调用请求是如何实现的呢?前面讲解了当client请求来自跨进程时,会创建一个Stub中的Proxy类的实例,我们在来看看这个Proxy类的源码,在前面的源码中可以看到Proxy类同样实现了IUser接口,先看看构造方法

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

在Stub的asInterface()方法中有这句代码,就是前面说的当请求为跨进程时创建Proxy的对象

return new sikang_demo.ipctest.IUser.Stub.Proxy(obj);

可以看到这里将obj作为构造参数,记录在了Proxy对象中,也就是说它持有了service的service引用,然后再看源码

@Override             public boolean login(java.lang.String userName, java.lang.String userPwd) throws android.os.RemoteException            {                /*用于存储方法参数和返回值*/                android.os.Parcel _data = android.os.Parcel.obtain();                android.os.Parcel _reply = android.os.Parcel.obtain();                boolean _result;                try {                    /*写入接口标识、和binder中login()方法需要的参数userName,和userPwd*/                    _data.writeInterfaceToken(DESCRIPTOR);                    _data.writeString(userName);                    _data.writeString(userPwd);                    /*调用mRemote的transact方法申请serive业务*/                    mRemote.transact(Stub.TRANSACTION_login, _data, _reply, 0);                    /*得到返回值*/                    _reply.readException();                    _result = (0!=_reply.readInt());                }finally {                    _reply.recycle();                    _data.recycle();                }                /*将结果反馈给客户端*/                return _result;            }            @Override             public void logout(java.lang.String userName) throws android.os.RemoteException{                android.os.Parcel _data = android.os.Parcel.obtain();                android.os.Parcel _reply = android.os.Parcel.obtain();                try {                    _data.writeInterfaceToken(DESCRIPTOR);                    _data.writeString(userName);                    mRemote.transact(Stub.TRANSACTION_logout, _data, _reply, 0);                    _reply.readException();                }finally {                    _reply.recycle();                    _data.recycle();                }            }

这里实现了IUser接口的两个方法,在跨进程的客户端请求binder方法业务时,直接与客户端接触的就是这里的方法,我们看看方法的内容,方法的处理是一样的,这里根据login方法来讲解

首先在方法开始创建了两个Parcel对象,我们知道Parcel是Java中序列化的一种实现,在跨进程通信时,传输的数据必须可序列化,这里这两个Parcel对象 _data_reply分别用于保存方法参数和接收返回值,可以看到在_data中写入了一个binder的接口标识,和login方法需要的两个参数,然后调用了

mRemote.transact(Stub.TRANSACTION_logout, _data, _reply, 0);

mRemote为Proxy被创建时传入的binder引用,先来看看这个方法几个参数的含义:

public final boolean transact(int code, Parcel data, Parcel reply, int flags)

code:请求方法的编号,当这个请求被service的binder收到时,就是通过这个参数来确定客户端请求的是哪个方法。
data:请求方法需要的参数
reply:用于接收方法的返回值
flags:一般用不到这个参数

data和reply很好理解,但这个code是什么呢?先看看Stub类的最下面有这么两句代码

static final int TRANSACTION_login = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);static final int TRANSACTION_logout = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);

这就是我们在IUser接口中定义的两个方法的code,由此可知,binder被创建时,会为它从接口继承来的每个方法都创建一个唯一的code,用来为方法编号,当client需要请求方法时,只需要向上面一样,传入一个方法code,及这个方法的参数和返回值保存者,就可以实现对指定方法的调用。
现在知道了client是这么请求的,那service又是如何响应的呢?看Stub中的onTransact类,

@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_login:                {                    /**取出方法参数*/                    data.enforceInterface(DESCRIPTOR);                    java.lang.String _arg0;                    _arg0 = data.readString();                    java.lang.String _arg1;                    _arg1 = data.readString();                    /**调用本地方法,传入参数*/                    boolean _result = this.login(_arg0, _arg1);                    reply.writeNoException();                    /**将返回值写入reply对象*/                    reply.writeInt(((_result)?(1):(0)));                    return true;                }                case TRANSACTION_logout:                {                    data.enforceInterface(DESCRIPTOR);                    java.lang.String _arg0;                    _arg0 = data.readString();                    this.logout(_arg0);                    reply.writeNoException();                    return true;                }            }            return super.onTransact(code, data, reply, flags);        }

onTransact()方法就是service的binder实体对client请求的响应方法,可以看到onTransact()的参数和client中调用的transact()方法参数相同,在client发出请求之后,Binder驱动 将这个请求通过ServiceManager提供的binder引用将请求转到binder的onTransact()方法中,如此service便受到了client的请求,然后在看看service是这么处理这个请求的

还是看login方法,看case TRANSACTION_login 中的处理:
首先取出了client写入到data中的参数,然后调用了login方法,最后将返回值写入了client提供的reply对象中,这时client就可以从reply中读取返回结果了

这就是Binder的请求处理过程,Binder就介绍到这,如果有什么考虑不周,大家可以帮忙提出来

更多相关文章

  1. Android开发 HTTP 发送 Post 与 Get 请求
  2. 史上最全的Android面试题集锦
  3. Android(安卓)getDimensionPixelSize, 代码中设置字体大小,读xml
  4. Android(安卓)IPC原理分析小结
  5. Android中ExpandableListView的使用(一)
  6. Android,谁动了我的内存(1)
  7. 浅谈Java中Collections.sort对List排序的两种方法
  8. 类和 Json对象
  9. Python list sort方法的具体使用

随机推荐

  1. 公众号文章目录整理
  2. android 休眠与唤醒II
  3. Android之 自定义属性 的使用
  4. 黑马程序员-Android(安卓)maps应用
  5. Android(安卓)新建一个lunch项(全志方案)
  6. NDK入门
  7. Android即用即查问题与知识点收藏
  8. 获取不到或者不更新intent传递的数据
  9. Android如何判断设备为Pad?
  10. Android(安卓)基于BaseActivity封装