二、IPC机制续(IPC方式)
来源:互联网 发布:唐诗逸和张傲月 知乎 编辑:程序博客网 时间:2024/05/02 00:06
IPC机制
具体方式有很多,比如可以在Intent中附加Extra来传递信息,或者通过共享文件的方式来共享数据,还可以采用Binder方式来跨进程通信,另外,Content Provider天生就是支持跨进程访问的,因此,我们也可以使用它来进行IPC,另外通过网络通信也是可以实现数据传递的,所以Socket也可以实现IPC。
1.使用Bundle
由于Bundle实现了Parcelable接口,所以它可以方便地在不同进程间传输。
除了直接传递数据这种典型的使用场景,他还有一种特殊的使用场景,如A进程正在进行计算,计算完成之后需要把结果传递给B进程,但是这个结果不支持放入Bundle中,那么可以这样考虑,A中,通过Intent启动B进程的一个Service组件(如IntentService),让Service进行后台计算,计算完毕之后,再启动B进程中真正想要启动的组件由于Service也在B进程中,所以目标组件就可以直接获取结果。
findViewById(R.id. button).setOnClickListener( new OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(); intent.setClass(MainActivity. this, SecondActivity.class); User user = new User(0, "jake", true); user. book = new Book(); intent.putExtra( "extra_user", (Serializable) user); startActivity( intent); } });
2.使用文件共享
两个进程通过读写同一个文化夹来交换数据,比如A进程把数据写进文件,B进程通过读取这个文件来获取数据。Linux使得并发读写文件可以没有限制,甚至两个线程同时对一个文件进行读写都是运行的。
希望在ManActivity中的onResume中序列化一个User对象到SDk卡上面的一个文件里面,在SecondActivity的onResume中去反序列化。
MainActivity:onResume执行下面的方法 private void persistToFile() { new Thread( new Runnable() { @Override public void run() { User user = new User(1, "hello world", false); File dir = new File(MyConstants. CHAPTER_2_PATH); if (! dir.exists()) { dir.mkdirs(); } File cachedFile = new File(MyConstants. CACHE_FILE_PATH ); ObjectOutputStream objectOutputStream = null; try { objectOutputStream = new ObjectOutputStream( new FileOutputStream(cachedFile)); objectOutputStream.writeObject( user); Log. d(TAG, "persist user:" + user); } catch (IOException e) { e.printStackTrace(); } finally { MyUtils. close(objectOutputStream); } } }).start(); }
SecondActivity中取: private void recoverFromFile() { new Thread(new Runnable() { @Override public void run() { User user = null; File cachedFile = new File(MyConstants. CACHE_FILE_PATH); if ( cachedFile.exists()) { ObjectInputStream objectInputStream = null; try { objectInputStream = new ObjectInputStream( new FileInputStream( cachedFile)); user = (User) objectInputStream.readObject(); Log. d(TAG, "recover user:" + user); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } finally { MyUtils. close(objectInputStream); } } } }).start(); }
当然这种不支持并发,如果想要并发,需要使用线程同步机制来解决。SharePreferences是个特例,通过键值对来存储数据,底层采用xml来存储键值对,位置在/data/data/packagename/shared_prefs目录下面,从本质来说SharePreferences也属于文件的一种,但是由于系统对它的读写有一定的缓存策略,即在内存里面有一份SharePreferences文件的缓存,因此在多进程模式下,系统对他的读写变得不可靠,当面对高并发的读写访问就有很大几率丢失数据,因此不建议进程间通信使用SP。
3.使用Messenger
Messenger是一种轻量级的IPC方案,它的底层实现是AIDL。从构造方法可以很明显的看出AIDL的痕迹。
public Messenger(Handler target) { mTarget = target.getIMessenger();}public Messenger(IBinder target) { mTarget = IMessenger.Stub.asInterface(target);}
Messenger的使用方法很简单,它对AIDL做了封装,使得我们可以更简单地进行线程间通信,同时由于它一次处理一个请求,因此在服务端我们不用考虑线程同步的问题,这个是因为服务端不存在并发执行的情况。
步骤:
1.服务端进程,首先我们要创建一个Service来处理客户端的请求,同时创建一个Handler并通过它来创建一个Messenger对象,在Service的onBind里面返回这个Messenger对象底层的Binder即可。
2.客户端进程,首先要绑定服务端的Service,绑定成功之后用服务端返回的IBinder对象创建一个Messenger,通过这个Messenger就可以向服务端发送消息了发送消息类型为Message对象。
如果要服务端能够回应客户端,就和服务端一样,需要创建一个Handler并创建一个新的Messenger,并把这个Messenger对象通过Message的replyTo参数传递给服务端,服务端通过这个replyTo参数就可以回应客户端。
服务端:public class MessengerService extends Service { private static final String TAG = "MessengerService"; private static class MessengerHandler extends Handler { @Override public void handleMessage(Message msg) { switch ( msg. what) { case MyConstants. MSG_FROM_CLIENT: Log. i(TAG, "receive msg from Client:" + msg.getData().getString( "msg")); Messenger client = msg. replyTo; Message relpyMessage = Message. obtain(null, MyConstants.MSG_FROM_SERVICE ); Bundle bundle = new Bundle(); bundle.putString( "reply", "嗯,你的消息我已经收到,稍后会回复你。" ); relpyMessage.setData( bundle); try { client. send(relpyMessage); } catch (RemoteException e) { e.printStackTrace(); } break; default: super.handleMessage( msg); } } } private final Messenger mMessenger = new Messenger( new MessengerHandler()); @Override public IBinder onBind(Intent intent) { return mMessenger.getBinder(); } @Override public void onCreate() { super.onCreate(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { return super.onStartCommand( intent, flags, startId); }}
客户端:public class MessengerActivity extends Activity { private static final String TAG = "MessengerActivity"; private Messenger mService; private Messenger mGetReplyMessenger = new Messenger( new MessengerHandler()); private static class MessengerHandler extends Handler { @Override public void handleMessage(Message msg) { switch ( msg. what) { case MyConstants. MSG_FROM_SERVICE: Log. i(TAG, "receive msg from Service:" + msg.getData().getString( "reply")); break; default: super.handleMessage( msg); } } } private ServiceConnection mConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { mService = new Messenger( service); Log. d(TAG, "bind service"); Message msg = Message. obtain(null, MyConstants.MSG_FROM_CLIENT ); Bundle data = new Bundle(); data.putString( "msg", "hello, this is client."); msg.setData( data); msg. replyTo = mGetReplyMessenger; try { mService.send( msg); } catch (RemoteException e) { e.printStackTrace(); } } public void onServiceDisconnected(ComponentName className) { } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate( savedInstanceState); setContentView(R.layout. activity_messenger); Intent intent = new Intent( "com.ryg.MessengerService.launch"); bindService( intent, mConnection, Context. BIND_AUTO_CREATE); } @Override protected void onDestroy() { unbindService( mConnection); super.onDestroy(); }}
Mainfest里面: <activity android:name= ".messenger.MessengerActivity" android:label= "@string/title_activity_messenger" > <intent-filter > <action android:name ="android.intent.action.MAIN" /> </intent-filter > </activity > <service android:name= ".messenger.MessengerService" android:process= ":remote" > <intent-filter > <action android:name ="com.ryg.MessengerService.launch" /> </intent-filter > </service >
注意:
通过Messenger来传递Message,Message中能用的载体只有what,arg1,arg2,Bundle以及replyTo。Message中的另外一个字段object在同一个进程中是很实用的,但是在跨进程间通信的时候,在Android2.2以前object字段不支持跨进程传输,
即使2.2以后,也仅仅是系统提供的实现了Parcelable接口的对象才能通过它来传输。这就意味着我们自定义的Parcelable对象是无法通过object字段来传输的。
Messenger的工作原理图:
4.使用AIDL
由于Messenger的主要作用还是传递消息,有时候可能需要跨进程调用服务端的方法,那么Messenger就不行了。
使用AIDL进行跨进程通信也分为客户端和服务端两个方面:
(1)服务端
服务端首先要创建一个Service用来监听客户端的连接请求,然后创建一个AIDL文件,将暴露给客户端的接口咋这个AIDL文件中声明,最后在Service中实现这个AIDL接口即可。
(2)客户端
首先绑定服务端的Service,绑定成功之后,将服务端返回的Binder对象转换成AIDL接口所属的类型,接着就可以调用AIDL中的方法了。
(3)AIDL接口的创建
package com.ryg.chapter_2.aidl;import com.ryg.chapter_2.aidl.Book;interface IBookManager { List<Book> getBookList(); void addBook(in Book book);}
AIDL文件支持的数据类型:
1)基本数据类型(int,long,char,boolean,double等)
2)String和CharSquence
3)List,只支持ArrayList,里面的每个元素都要必须能被AIDL支持
4)Map,只支持HashMap,里面的每个元素都必须被AIDL支持,
包括Key和Value
5)Parcelable,所有实现了Parcelable接口的对象
6)AIDL,所有的AIDL接口本身也可以在AIDL文件中使用
以上的6种数据类型就是AIDL所支持的所有类型,其中自定义的Parcelable对象和AIDL对象必须要显式import进来,不管是否和当前的AIDL文件位于同一个包里面。
另外,如果AIDL文件里面用到了自定义的Parcelable对象,那么必须新建一个和它同名的AIDL文件,并在里面声明它为Parcelable类型。
在IBookManager.aidl文件中使用到了自定义的Book对象,所以必须创建Book.aidl在里面添加:
package com.ryg.chapter_2.aidl;parcelable Book;
AIDL中每个实现了Parcelable接口的类型的类,都需要像上面那样去声明,创建对应的AIDL文件,并声明那个类为parcelable。除此之外,AIDL中除了基本数据类型,其他类型的参数必须标上方向,in,out或者inout,in表示输入型参数,out表示输出型参数,inout表示输入输出型参数。AIDL接口中只支持方法,不支持静态常量。
in,out,inout区别:
http://hold-on.iteye.com/blog/2026138
为了方便开发,建议把所有的和AIDL相关的类和文件全部放入同一个包中,这样的好处是,当客户端是另外的应用时,我们可以直接把整个包复制放入到客户端工程中。
AIDL包结构,在客户端和服务端要一致,否则会运行出错,这是因为客户端需要反序列化服务端中和AIDL接口相关的所有类。
(4)远程服务端的Service实现
public class BookManagerService extends Service { private static final String TAG = "BMS"; private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<Book>(); private Binder mBinder = new IBookManager.Stub() { @Override public List<Book> getBookList() throws RemoteException { return mBookList; } @Override public void addBook(Book book) throws RemoteException { mBookList.add( book); } }; @Override public void onCreate() { super.onCreate(); mBookList.add(new Book(1, "Android")); mBookList.add(new Book(2, "Ios")); } @Override public IBinder onBind(Intent intent) { return mBinder; }}
<service android:name= ".aidl.BookManagerService" android:process= ":remote" > </service >
采用CopyOnWriteArrayList,它支持并发读写,AIDL方法是在服务端的Binder线程池中执行的,因此当多个客户端同时连接的时候,会存在多个线程同时访问的问题,所以要在AIDL方法中处理线程同步,这里使用它来进行自动的线程同步。
服务端可以使用CopyOnWriteArrayList和ConcurrentHashMap来进行自动线程同步,客户端拿到的依然是ArrayList和HashMap。
(5)客户端的实现
客户端首先绑定远程服务,绑定成功之后将服务端返回的Binder对象转换成为AIDL接口,然后就可以通过这个接口去调用服务端的远程方法了。
public class BookManagerActivity extends Activity { private static final String TAG = "BookManagerActivity"; private ServiceConnection mConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { IBookManager bookManager = IBookManager.Stub.asInterface(service); try { List<Book> list = bookManager.getBookList(); Log. i(TAG, "query book list, list type:" + list.getClass().getCanonicalName()); Log. i(TAG, "query book list:" + list.toString()); } catch (RemoteException e) { e.printStackTrace(); } } public void onServiceDisconnected(ComponentName className) { } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate( savedInstanceState); setContentView(R.layout. activity_book_manager); Intent intent = new Intent( this, BookManagerService. class); bindService( intent, mConnection, Context. BIND_AUTO_CREATE); } @Override protected void onDestroy() { unbindService( mConnection); super.onDestroy(); }}
(6)监听
比如现在想服务端有新书的时候通知客户端,那么必须要监听了需要使用RemoteCallbackList,存储我们自定义的监听器,它是一个泛型,支持管理任意的AIDL接口。它的内部是Map结构,key是IBinder类型,value是Callback类型。
注意:
服务端和客户端之间做监听器,服务端需要使用RemoteCallbackList,否则客户端的监听器无法收到通知(因为服务端实质还是一份新的序列化后的监听器实例,并不是客户端那份)
RemoteCallbackList的beginBroadcast和finishBroadcast必须配对使用,哪怕我们仅仅需要获取RemoteCallbackList中的元素个数。
(7)不要在客户端的ui线程里面调用服务端的耗时方法
客户端调用远程服务方法时,因为远程方法运行在服务端的binder线程池中(服务端方法可以执行大量耗时操作,不需要开线程执行异步任务的);
同时客户端线程会被挂起,所以如果该方法过于耗时,而客户端又是UI线程,会导致ANR,所以当确认该远程方法是耗时操作时,应避免客户端在UI线程中调用该方法。
同理,当服务器调用客户端的listener方法时,该方法也运行在客户端的binder线程池中,所以如果该方法也是耗时操作,请确认运行在服务端的非UI线程中。
另外,因为客户端的回调listener运行在binder线程池中,所以更新UI需要用到handler。
(8)服务端进程意外终止
客户端通过IBinder.DeathRecipient来监听Binder死亡,也可以在onServiceDisconnected中监听并重连服务端。区别在于前者是在binder线程池中,访问UI需要用Handler,后者则是UI线程。
(9)权限验证
可通过自定义权限在onBind或者onTransact中进行权限验证。
onBind中验证,验证不通过返回null,验证方式可以使用permission验证,首先在manifest里面注册。
<permission android:name= "com.ryg.chapter_2.permission.ACCESS_BOOK_SERVICE" android:protectionLevel= "normal" />
就可以在onBind里面验证了。
@Override public IBinder onBind(Intent intent) { int check = checkCallingOrSelfPermission("com.ryg.chapter_2.permission.ACCESS_BOOK_SERVICE" ); Log. d(TAG, "onbind check=" + check); if ( check == PackageManager. PERMISSION_DENIED) { return null; } return mBinder; }
一个应用来绑定我们的服务的时候,会验证这个应用的权限,没有权限就返回null。这个方法同样适用于Messenger中。
我们自己内部的应用想要绑定我们的服务,只需要在Manifest采用如下方式使用permission即可。
<uses-permission android:name="com.ryg.chapter_2.permission.ACCESS_BOOK_SERVICE" />
服务端的onTransact方法中进行权限验证,验证失败就会返回false,服务端的方法就不会执行,验证方式可以采用permission验证,也可以使用Uid和Pid来验证。通过getCallingUid和getCallingPid可以拿到客户端所属的应用的Uid和Pid,通过这两个参数可以做一些验证工作,比如验证包名。
下面即验证了权限又需要包名以com.rgy开始。
public boolean onTransact( int code, Parcel data, Parcel reply, int flags) throws RemoteException { int check = checkCallingOrSelfPermission( "com.ryg.chapter_2.permission.ACCESS_BOOK_SERVICE" ); Log. d(TAG, "check=" + check); if ( check == PackageManager. PERMISSION_DENIED) { return false; } String packageName = null; String[] packages = getPackageManager().getPackagesForUid( getCallingUid()); if ( packages != null && packages. length > 0) { packageName = packages[0]; } Log. d(TAG, "onTransact: " + packageName); if (! packageName.startsWith( "com.ryg")) { return false; } return super.onTransact( code, data, reply, flags); }
5.使用ContentProvider
底层实现是AIDL,使用比AIDL简单许多。自定义ContentProvider需要继承ContentProvider并实现里面的方法即可,onCreate,query,update,insert,delete,getType,getType用来返回Uri请求所对应的MimeType。如果应用不关系这个,只需要返回null或者”/“,根据Binder,我们知道这6个方法均运行在ContentProvider的进程里面,除了onCreate方法由系统回调并运行在主线程里面,其余的5个均由外界调用并运行在Binder线程池中。
虽然ContentProvider的底层数据看起来很像一个SQLite数据库,但是它对底层的数据的存储方式没有任何要求,我们即可以使用SQLite,也可以使用普通文件,甚至可以采用内存中的一个对象来进行数据的存储。
注册android:authorities是它的唯一标识,建议命名的时候加上包名前缀,如果声明了权限,那么外界应用也需要相应的权限。
ContentProvider(有的手机上会出现不加uses-permission依然可以访问BookProvider的问题)
6.使用Socket
Socket 一般用于网络通信,AIDL用这种方式会过于繁琐,不建议。
7.Binder连接池
比如100个地方需要用到AIDL那么不可能创建100个Service,需要减少Service的数量,将AIDL放在同一个Service里面去管理。
Binder连接池的作用就是将每个业务模块的Binder请求统一转发到远程的Service中去。
Binder连接池,通过BinderPool的方式将Binder的控制与Service本身解耦,同时只需要维护一份Service即可。这里用到了CountDownLatch,大概解释下用意:线程在await后等待,直到CountDownLatch的计数为0,BinderPool里使用它的目的是为了保证Activity获取BinderPool的时候Service已确定bind完成~
例子:
两个AIDL:
ISecurityCenter.aidlpackage com.ryg.chapter_2.binderpool;interface ISecurityCenter { String encrypt(String content); String decrypt(String password);}
ICompute.aidlpackage com.ryg.chapter_2.binderpool;interface ICompute { int add( int a, int b);}
实现:
public class SecurityCenterImpl extends ISecurityCenter.Stub { private static final char SECRET_CODE = '^'; @Override public String encrypt(String content) throws RemoteException { char[] chars = content.toCharArray(); for (int i = 0; i < chars.length; i++) { chars[ i] ^= SECRET_CODE; } return new String(chars); } @Override public String decrypt (String password ) throws RemoteException { return encrypt( password); }}
以及:
public class ComputeImpl extends ICompute.Stub { @Override public int add(int a, int b) throws RemoteException { return a + b; }}
为Binder连接池创建AIDL接口IBinderPool.aidl
interface IBinderPool { /** * @param binderCode, the unique token of specific Binder<br/> * @return specific Binder who's token is binderCode. */ IBinder queryBinder( int binderCode);}
为Binder连接池创建远程Service并实现IBinderPool,
下面是queryBinder的实现:
@Override public IBinder queryBinder( int binderCode) throws RemoteException { IBinder binder = null; switch ( binderCode) { case BINDER_SECURITY_CENTER: { binder = new SecurityCenterImpl(); break; } case BINDER_COMPUTE: { binder = new ComputeImpl(); break; } default: break; } return binder; }
远程Service的实现比较简单了:
public class BinderPoolService extends Service { private static final String TAG = "BinderPoolService"; private Binder mBinderPool = new BinderPool.BinderPoolImpl(); @Override public void onCreate() { super.onCreate(); } @Override public IBinder onBind(Intent intent ) { Log.d(TAG, "onBind"); return mBinderPool; } @Override public void onDestroy() { super.onDestroy(); }}
下面还有Binder连接池的具体实现,首先绑定远程服务,成功之后,客户端就可以通过它的queryBinder方法获取各自对应的Binder,拿到所需要的Binder之后,不同的业务模块之间就可以进行各自的操作了。
public class BinderPool { private static final String TAG = "BinderPool"; public static final int BINDER_NONE = -1; public static final int BINDER_COMPUTE = 0; public static final int BINDER_SECURITY_CENTER = 1; private Context mContext; private IBinderPool mBinderPool; private static volatile BinderPool sInstance; private CountDownLatch mConnectBinderPoolCountDownLatch; private BinderPool(Context context) { mContext = context.getApplicationContext(); connectBinderPoolService(); } public static BinderPool getInsance(Context context) { if (sInstance == null) { synchronized (BinderPool.class) { if (sInstance == null) { sInstance = new BinderPool(context); } } } return sInstance; } private synchronized void connectBinderPoolService() { mConnectBinderPoolCountDownLatch = new CountDownLatch(1); Intent service = new Intent(mContext, BinderPoolService.class); mContext.bindService(service, mBinderPoolConnection, Context.BIND_AUTO_CREATE); try { mConnectBinderPoolCountDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } } /** * query binder by binderCode from binder pool * * @param binderCode * the unique token of binder * @return binder who's token is binderCode<br> * return null when not found or BinderPoolService died. */ public IBinder queryBinder(int binderCode) { IBinder binder = null; try { if (mBinderPool != null) { binder = mBinderPool.queryBinder(binderCode); } } catch (RemoteException e) { e.printStackTrace(); } return binder; } private ServiceConnection mBinderPoolConnection = new ServiceConnection() { @Override public void onServiceDisconnected(ComponentName name) { // ignored. } @Override public void onServiceConnected(ComponentName name, IBinder service) { mBinderPool = IBinderPool.Stub.asInterface(service); try { mBinderPool.asBinder().linkToDeath(mBinderPoolDeathRecipient, 0); } catch (RemoteException e) { e.printStackTrace(); } mConnectBinderPoolCountDownLatch.countDown(); } }; private IBinder.DeathRecipient mBinderPoolDeathRecipient = new IBinder.DeathRecipient() { @Override public void binderDied() { Log.w(TAG, "binder died."); mBinderPool.asBinder().unlinkToDeath(mBinderPoolDeathRecipient, 0); mBinderPool = null; connectBinderPoolService(); } }; public static class BinderPoolImpl extends IBinderPool.Stub { public BinderPoolImpl() { super(); } @Override public IBinder queryBinder(int binderCode) throws RemoteException { IBinder binder = null; switch (binderCode) { case BINDER_SECURITY_CENTER: { binder = new SecurityCenterImpl(); break; } case BINDER_COMPUTE: { binder = new ComputeImpl(); break; } default: break; } return binder; } }}
使用:
public class BinderPoolActivity extends Activity { private static final String TAG = "BinderPoolActivity"; private ISecurityCenter mSecurityCenter; private ICompute mCompute; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate( savedInstanceState); setContentView(R.layout. activity_binder_pool); new Thread(new Runnable() { @Override public void run() { doWork(); } }).start(); } private void doWork() { BinderPool binderPool = BinderPool.getInsance(BinderPoolActivity. this); IBinder securityBinder = binderPool .queryBinder(BinderPool. BINDER_SECURITY_CENTER); mSecurityCenter = (ISecurityCenter) SecurityCenterImpl . asInterface(securityBinder); Log.d(TAG, "visit ISecurityCenter"); String msg = "helloworld-安卓"; System. out.println( "content:" + msg); try { String password = mSecurityCenter.encrypt( msg); System. out.println( "encrypt:" + password); System. out.println( "decrypt:" + mSecurityCenter.decrypt(password )); } catch (RemoteException e) { e.printStackTrace(); } Log.d(TAG, "visit ICompute"); IBinder computeBinder = binderPool .queryBinder(BinderPool. BINDER_COMPUTE); mCompute = ComputeImpl.asInterface(computeBinder ); try { System. out.println( "3+5=" + mCompute.add(3, 5)); } catch (RemoteException e) { e.printStackTrace(); } }}
8.选择适合的IPC方式
- 二、IPC机制续(IPC方式)
- 二、IPC机制续(IPC方式)
- IPC 机制(二)
- IPC机制(二)
- IPC机制(二)
- 二、IPC机制(IPC介绍)
- 【Android】IPC机制(二)
- android IPC机制(二)
- Android IPC机制(二)
- IPC机制之四:IPC方式(AIDL)
- IPC机制---04 Android中的IPC通讯方式(A)
- IPC机制---04 Android中的IPC通讯方式(B)
- IPC机制---04 Android中的IPC通讯方式(C)
- IPC机制---04 Android中的IPC通讯方式(D)
- IPC机制---04 Android中的IPC通讯方式(E)
- 4.IPC 机制(二) IPC基础概念介绍
- 读书笔记--IPC机制(二)
- IPC机制<二>AIDL
- Ogre的渲染流程,在渲染时材质是如何起作用的,材质加载和解析
- raw_input() 与 input() __ Python
- Node 笔记
- 丰炜(VIGOR)PLC感受:
- Struts-文件上传
- 二、IPC机制续(IPC方式)
- python IDLE入门
- Windows下Mysql5.6启动日志
- BLOG搬家
- android 布局如何支持多种不同屏幕尺寸
- struts2中Included file cannot be found问题解决
- ios UITableView 异步加载图片并防止错位
- tableView头部放scrollView滚动视图
- Objective-C Foundation框架实践——NSArray(二)