編輯:關於android開發
AIDL與其他IDL語言類似,你需要做一些工作。 它允許你定義客戶端與服務端達成一致的程序接口使用進程間通信相互交流。 在ANdroid上面,一個進程不能正常的訪問另一個進程的內存。 所以說,他們需要分解他們的對象為操作系統可以理解的基本單位,然後為你把這些對象按次序跨越進程邊界 書寫這些代碼是單調冗長的,所以android使用AIDL為你處理這個問題。
注意:使用AIDL只有在你允許來自不同應用的客戶端跨進程通信訪問你的service,並且想要在你的service種處理多線程的時候才是必要的。 如果你不需要執行不同應用之間的IPC並發,你應該通過實現Binder建立你的接口,或者如果你想執行IPC,但是不需要處理多線程。那麼使用Messenger實現你的接口 不管怎樣,確保你在實現一個AIDL之前理解了Bound Service
在你設計你的AIDL接口之前,請注意調用一個AIDL接口是直接的函數調用 你不應該假設線程在哪個調用中發生 情形與依賴調用是來自一個本地進程中的線程還是一個遠程進程中的線程相關 尤其是:
來自本地進程的調用與調用者在同一個線程中執行。 如果這是你的主UI線程,線程繼續在AIDL接口中執行 如果是其他的線程,則它是一個在service中執行你的代碼的線程 這樣,如果只是本地線程訪問這個service,你完全可以控制哪些線程在其中執行(但是如果是那樣的話,那麼你壓根就不應該使用AIDL,而應該通過實現Binder建立接口) 平台在你自己的進程中內部維護一個線程池中分配的遠程進程的調用 你必須為從未知線程發出的即將到來的調用,並且是伴隨同時多個調用做好准備 換句話說,AIDL接口的實現必須是完全的線程安全的 單向關鍵詞限定了遠程調用的行為 使用的時候,一個遠程調用不會被阻塞;它只是簡單的發送傳輸數據並且立即返回 最終接口的實現把它作為一個來自Binder線程池的常規調用、一個普通的遠程調用來接收 如果本地調用使用單向的,那麼就不會有影響,並且調用仍然是異步的你必須在一個.aidl文件中使用java編程語言語法定義你的AIDL接口,然後在提供service的應用中和任何綁定到這個service的應用中的源代碼中(在src目錄嚇)保存它
當你編譯包含.aidl文件的應用時,Android SDK工具基於這個.aidl文件生成一個IBinder接口,並且把它保存到項目的gen目錄嚇 service必須恰當的實現這個IBinder接口 之後客戶端應用可以綁定到這個服務上,然後從IBinder調用方法來執行IPC
使用AIDL建立一個鄰接的service需要遵循下面的步驟
1.建立.aidl文件這個文件使用方法簽名定義了語言接口
2.實現這個接口Android SDk工具基於你的.aidl文件使用java語言生成一個接口 這個接口有一個內部抽象類,叫做Stub,它是繼承Binder並且實現你AIDL接口的 你必須繼承這個Stub類並且實現這些方法
3.暴露這個接口給客戶端實現一個service並且覆蓋onBind()方法返回你的Stub實現類
警告:在你第一次發布AIDL之後的其中任何的改變必須保持向後兼容來避免破壞其他應用程序使用你的service 也就是說,因為你的.aidl文件必須被復制到其他應用程序中來讓他們訪問你service的接口,你必須維護原始接口的支持。
AIDL使用一個簡單的語法讓你聲明一個帶有一個或者多個帶有參數和返回值方法的接口 參數和返回值可以是任何類型,甚至是AIDL生成的接口
你必須使用java語言構建.aidl文件 每一個.aidl文件必須定義一個簡單的接口並且要求只有接口聲明和方法簽名
默認的,AIDL支持下面數據類型:
ava語言中的所有基本數據類型(比如int、long、char、boolean等等)String
CharSequence
List
List中的所有元素必須是AIDL支持的類型之一,或者是一個其他AIDL生成的接口,或者是你定義的parcelable List可以使用范型(例如,List) 接收端的實際類經常是一個ArrayList,盡管方法是使用List接口生成的
Map
Map中的所有元素必須是AIDL支持的類型之一,或者是一個其他AIDL生成的接口,或者是你定義的parcelable 范型map是不被支持的(比如這種形式Map) 接收端的實際類經常是一個HashMap,盡管方法是使用Map接口生成的
對於上述類型之外的類型,你必須聲明import
,即使在同一個包內。
當定義你的service接口的時候,注意:
方法可以接收0或多個參數,並且有返回值或者返回void 所有非基本數據類型要求要求一個定向的tag來指定數據是去往哪個方向的 無論是輸入、輸出,還是輸入輸出(參加下面的例子) 基本數據類型是默認支持的,並且不能是其他的。
警告:你應該限制方向於真正需要的地方,因為排列整理參數的開銷是很昂貴的。
.aidl文件中的所有的代碼注釋都在生成的IBinder接口中(除了在import和包聲明之前的注釋) 只支持方法,你不可以在AIDL暴露靜態域這有個.aidl文件的例子:
// IRemoteService.aidl package com.example.android; // Declare any non-default types here with import statements /** Example service interface */ interface IRemoteService { /** Request the process ID of this service, to do evil things with it. */ int getPid(); /** Demonstrates some basic types that you can use as parameters * and return values in AIDL. */ void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString); }
簡單的保存你的.aidl文件在你工程的src目錄下,當你build你的應用時,SDK工具在你工程的gen目錄下生成IBinder接口文件 生成的文件名字與.aidl名字匹配,但是是以.java為擴展名(例如IRemoteService.aidl對應為IRemoteService.java)
如果你使用Eclipse,增量編譯幾乎是立刻生成binder類。如果你不使用Eclipse,那麼Ant工具在你下次編譯你的應用(你應該使用ant debug或者ant release編譯你的工程)時生成binder類。一旦你寫好了.aidl文件,你的代碼就可以鏈接到生成的類上面了。
當你編譯你的應用時,Android SDK工具生成一個.java接口文件用你的.aidl文件命名 生成的接口包含一個名字為Stub的子類(比如YourInterface.Stub),這是一個它父類的抽象實現,並且聲明了.aidl中所有的方法
注意:Stub也定義了一些輔助的方法,最顯著的就是asInterface(),它是用來接收一個IBinder(通常IBinder傳遞給客戶端的onServiceConnected()回調方法)並且返回一個Stub接口的實例 更多細節參考Calling an IPC Method章節。
為了實現來自.aidl文件生成的接口,需要繼承Binder接口(例如YourInterface.Stub)並且實現從.aidl文件中繼承的方法。
這有一個使用匿名實例實現一個叫IRemoteService(定義在IRemoteService.aidl中,例子如上)的接口的例子
private final IRemoteService.Stub mBinder = new IRemoteService.Stub() { public int getPid(){ return Process.myPid(); } public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) { // Does nothing } };
mBinder是一個Stub類的實例
當實現你的AIDL接口的時候有很多規則需要注意
調用不保證在主線程中執行,所以你需要一開始就考慮多線程並且適當的build你的service為線程安全的 默認的,RPC調用是同步的。 如果你知道service需要花費一些時間來完成請求,你就不應該從activity的主線程中調用它,因為它可能使得應用沒有響應(Android也許會顯示一個ANR的對話框),通常你應該在客戶端中一個單獨的線程調用它 拋出的異常不會返回給調用者一旦你為service實現了接口,你需要把它暴露給客戶端,這樣他們才能綁定到上面 為了給你的service暴露接口,繼承Service並且實現onBind()方法返回一個你實現生成的Stub類(像我們在上一結討論的那樣) 這有一個service暴露IRemoteService接口給客戶端的例子
public class RemoteService extends Service { @Override public void onCreate() { super.onCreate(); } @Override public IBinder onBind(Intent intent) { // Return the interface return mBinder; } private final IRemoteService.Stub mBinder = new IRemoteService.Stub() { public int getPid(){ return Process.myPid(); } public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) { // Does nothing } }; }
現在,當一個客戶端(比如一個activity)調用bindService()來連接到這個service,這個客戶端的onServiceConnected()回調函數接收service中onBind()方法返回的mBinder實例
客戶端必須可以訪問接口類,所以如果客戶端和服務端在不同的應用中,那麼客戶端所在的應用必須有一份.aidl文件的副本在其src目錄下(生成android.os.Binder接口,提供客戶端訪問AIDL方法都在這個目錄下)
當客戶端在onServiceConnected()回調方法中接收到IBinder時,它必須調用你的ServiceInterface.Stub.asInterface(service)來把返回參數映射到你的ServiceInterface類型上。例如:
IRemoteService mIRemoteService; private ServiceConnection mConnection = new ServiceConnection() { // Called when the connection with the service is established public void onServiceConnected(ComponentName className, IBinder service) { // Following the example above for an AIDL interface, // this gets an instance of the IRemoteInterface, which we can use to call on the service mIRemoteService = IRemoteService.Stub.asInterface(service); } // Called when the connection with the service disconnects unexpectedly public void onServiceDisconnected(ComponentName className) { Log.e(TAG, "Service has unexpectedly disconnected"); mIRemoteService = null; } };
更多樣本代碼參見ApiDemos中的RemoteService.java
如果你想通過IPC接口把一個類從一個進程傳遞到另一個進程中,那麼是可以的。 然而,你必須保證為你的類而寫的代碼也是對IPC通道另一端是可用的,並且你的類必須支持Parcelable接口 支持Parcelable接口是很重要的,因為它允許Android系統把對象分解為可以被組織跨進程傳輸基本單元
為了建立一個支持Parcelable協議的類,你必須遵守下面的規則:
要實現Parcelable接口 實現writeToParcel,它是用來把對象的當前狀態寫入到一個Parcel對象中的。 在你的類中添加一個叫CREATOR的靜態域,它要實現Parcelable.Creator接口 最後,建立一個.aidl文件聲明你的parcelable類(如下面的Rect.aidl所示)如果你使用一個定制的構建過程,不要構建.aidl文件。與C語言中的頭文件類似,.aidl文件不會被編譯
AIDL使用代碼中的這些域和方法封裝傳送和解讀你的對象
例如,這有一個Rect.aidl文件類建立一個Rect類,它是parcelable的
package android.graphics; // Declare Rect so AIDL can find it and knows that it implements // the parcelable protocol. parcelable Rect;
這有一個Rect類如何實現Parcelable協議的例子
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.CreatorCREATOR = new Parcelable.Creator () { 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(); } }
在Rect類中組織傳送數據是很簡單的 看一下Parcel上面的其他函數,看看你可以如何將其他類型的值寫入一個Parcel中。
警告:不要忘記從其他進程接收數據的安全本質性 這種情況下,Rect從Parcel中讀取4個數字,但是這取決於你要保證他們在可接收范圍之內而不管調用者到底試圖要做些什麼 獲取更多關於如何遠離惡意程序保證你應用安全的更多信息,參見Security and Permissions
下面是調用步驟,調用者必須調用一個AIDL定義的遠程接口
在項目中的src目錄下面導入.aidl文件 聲明一個IBinder接口(基於AIDL生成的)的實例 實現ServiceConnection 調用Context.bindService(),傳遞到你的ServiceConnection實現中。 在你的onServiceConnected()實現中,你會收到一個IBinder實例(稱為服務端) 調用YourInterfaceName.Stub.asInterface((IBinder)service)把返回值映射到YourInterface類型上面 調用你接口中定義的方法 你應該捕獲當連接損壞時拋出的DeadObjectException異常,這是遠程方法唯一會拋出的異常 使用你接口的實例調用Context.unbindService()來斷開連接調用IPC服務端的一些注釋:
對象跨進程時是引用計數的 你可以傳遞一個匿名對象作為方法的參數更多綁定service的信息請閱讀Bound Services文檔
調用一個AIDL建立的服務端的一些樣本代碼,來自ApiDemos工程中的Remote Service樣本。
public static class Binding extends Activity { /** The primary interface we will be calling on the service. */ IRemoteService mService = null; /** Another interface we use on the service. */ ISecondary mSecondaryService = null; Button mKillButton; TextView mCallbackText; private boolean mIsBound; /** * Standard initialization of this activity. Set up the UI, then wait * for the user to poke it before doing anything. */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.remote_service_binding); // Watch for button clicks. Button button = (Button)findViewById(R.id.bind); button.setOnClickListener(mBindListener); button = (Button)findViewById(R.id.unbind); button.setOnClickListener(mUnbindListener); mKillButton = (Button)findViewById(R.id.kill); mKillButton.setOnClickListener(mKillListener); mKillButton.setEnabled(false); mCallbackText = (TextView)findViewById(R.id.callback); mCallbackText.setText("Not attached."); } /** * Class for interacting with the main interface of the service. */ private ServiceConnection mConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { // This is called when the connection with the service has been // established, giving us the service object we can use to // interact with the service. We are communicating with our // service through an IDL interface, so get a client-side // representation of that from the raw service object. mService = IRemoteService.Stub.asInterface(service); mKillButton.setEnabled(true); mCallbackText.setText("Attached."); // We want to monitor the service for as long as we are // connected to it. try { mService.registerCallback(mCallback); } catch (RemoteException e) { // In this case the service has crashed before we could even // do anything with it; we can count on soon being // disconnected (and then reconnected if it can be restarted) // so there is no need to do anything here. } // As part of the sample, tell the user what happened. Toast.makeText(Binding.this, R.string.remote_service_connected, Toast.LENGTH_SHORT).show(); } public void onServiceDisconnected(ComponentName className) { // This is called when the connection with the service has been // unexpectedly disconnected -- that is, its process crashed. mService = null; mKillButton.setEnabled(false); mCallbackText.setText("Disconnected."); // As part of the sample, tell the user what happened. Toast.makeText(Binding.this, R.string.remote_service_disconnected, Toast.LENGTH_SHORT).show(); } }; /** * Class for interacting with the secondary interface of the service. */ private ServiceConnection mSecondaryConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { // Connecting to a secondary interface is the same as any // other interface. mSecondaryService = ISecondary.Stub.asInterface(service); mKillButton.setEnabled(true); } public void onServiceDisconnected(ComponentName className) { mSecondaryService = null; mKillButton.setEnabled(false); } }; private OnClickListener mBindListener = new OnClickListener() { public void onClick(View v) { // Establish a couple connections with the service, binding // by interface names. This allows other applications to be // installed that replace the remote service by implementing // the same interface. bindService(new Intent(IRemoteService.class.getName()), mConnection, Context.BIND_AUTO_CREATE); bindService(new Intent(ISecondary.class.getName()), mSecondaryConnection, Context.BIND_AUTO_CREATE); mIsBound = true; mCallbackText.setText("Binding."); } }; private OnClickListener mUnbindListener = new OnClickListener() { public void onClick(View v) { if (mIsBound) { // If we have received the service, and hence registered with // it, then now is the time to unregister. if (mService != null) { try { mService.unregisterCallback(mCallback); } catch (RemoteException e) { // There is nothing special we need to do if the service // has crashed. } } // Detach our existing connection. unbindService(mConnection); unbindService(mSecondaryConnection); mKillButton.setEnabled(false); mIsBound = false; mCallbackText.setText("Unbinding."); } } }; private OnClickListener mKillListener = new OnClickListener() { public void onClick(View v) { // To kill the process hosting our service, we need to know its // PID. Conveniently our service has a call that will return // to us that information. if (mSecondaryService != null) { try { int pid = mSecondaryService.getPid(); // Note that, though this API allows us to request to // kill any process based on its PID, the kernel will // still impose standard restrictions on which PIDs you // are actually able to kill. Typically this means only // the process running your application and any additional // processes created by that app as shown here; packages // sharing a common UID will also be able to kill each // other's processes. Process.killProcess(pid); mCallbackText.setText("Killed service process."); } catch (RemoteException ex) { // Recover gracefully from the process hosting the // server dying. // Just for purposes of the sample, put up a notification. Toast.makeText(Binding.this, R.string.remote_call_failed, Toast.LENGTH_SHORT).show(); } } } }; // ---------------------------------------------------------------------- // Code showing how to deal with callbacks. // ---------------------------------------------------------------------- /** * This implementation is used to receive callbacks from the remote * service. */ private IRemoteServiceCallback mCallback = new IRemoteServiceCallback.Stub() { /** * This is called by the remote service regularly to tell us about * new values. Note that IPC calls are dispatched through a thread * pool running in each process, so the code executing here will * NOT be running in our main thread like most other things -- so, * to update the UI, we need to use a Handler to hop over there. */ public void valueChanged(int value) { mHandler.sendMessage(mHandler.obtainMessage(BUMP_MSG, value, 0)); } }; private static final int BUMP_MSG = 1; private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case BUMP_MSG: mCallbackText.setText("Received from service: " + msg.arg1); break; default: super.handleMessage(msg); } } }; }
1.基於前面寫的aidl使用,這段時間准備研究ActivityManager框架,對aidl進行了更深入的研究,因為android框架大量使用了進程通信機制,所以,在研究android framework前認真研究一下AIDL的實現機制十分有必要的
2.前面講了aidl是 Android Interface definition language的縮寫,它是一種進程通信接口的描述,通過sdk解釋器對器進行編譯,會把它編譯成java代碼在gen目錄下,類路徑與aidl文件的類路徑相同。
3.aidl接口
package com.cao.android.demos.binder.aidl;
import com.cao.android.demos.binder.aidl.AIDLActivity;
interface AIDLService {
void registerTestCall(AIDLActivity cb);
void invokCallBack();
}
它編譯後生成的java文件如下
AIDLService.java詳細描述了aidl接口的實現,看上面圖示,AIDLActivity.aidl編譯成了一個接口AIDLActivity,一個存根類Stub,一個代理類Proxy
public interface AIDLService extends android.os.IInterface//與AIDLActivity.aidl中定義的接口對應的java接口實現
public static abstract class Stub extends android.os.Binder implements com.cao.android.demos.binder.aidl.AIDLService
//繼承android.os.Binder,在onTransact完成對通信數據的接收,通過不同通信參數code調用AIDLService接口方法,並回寫調用返回結果AIDLService接口方法需要在
//服務端實現
private static class Proxy implements com.cao.android.demos.binder.aidl.AIDLService
//實現AIDLService接口方法,但是方法只是執行代理遠程調用操作,具體方法操作在遠端的Stub存根類中實現
總的來說,AIDLActivity.aidl編譯會生成一個AIDLActivity接口,一個stub存根抽像類,一個proxy代理類,這個實現其實根axis的wsdl文件編譯生成思路是一致的,
stub存根抽像類需要在服務端實現,proxy代理類被客戶端使用,通過stub,proxy的封裝,屏蔽了進程通信的細節,對使用者來說就只是一個AIDLActivity接口的調用
4.根據以上思路使用aidl再看一下AIDLService調用實現代碼
--1.在服務端實現AIDLService.Stub抽象類,在服務端onBind方法中返回該實現類
--2.客戶端綁定service時在ServiceConnection.onServiceConnected獲取onBind返回的IBinder對象
private ServiceConnection mConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
Log("connect service");
mService = AIDLService.Stub.asInterface(service);
try {
mService.registerTestCall(mCallback);
} catch (RemoteException e) {
}
}
注意mConnection在bindservice作為調用參數:bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
--3.AIDLService.Stub.asInterface(service);
public static com.cao.android.demos.binder.aidl.AIDLService asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR);
//如果bindService綁定的是同一進程的service,返回的是服務端Stub對象本省,那麼在客戶端是直接操作Stub對象,並不進行進程通信了
if (((iin!=null)&&(iin instanceof com.cao.android.demos.binder.aidl.AIDLService))) {
return ((com.cao.android.demos.binder.aidl.AIDLService)iin);
}
//bindService綁定的不是同一進程的service,返回的是代理對象,obj==android.os.BinderProxy對象,被包裝成一個AIDLService.Stub.Proxy代理對象
//不過AIDLService.Stub.Proxy進程間通信通過android.os.BinderProxy實現
return new com.cao.android.demos.binder.aidl.AIDLService.Stub.Proxy(obj);
}
--4.調用AIDLService接口方法,如果是同一進程,AIDLService就是service的Stub對象,等同直接調用Stub對象實現的AIDLService接口方法
如果是一個proxy對象,那就是在進程間調用了,我們看一個客戶端調用的例子:
public void onClick(View v) {
Log("AIDLTestActivity.btnCallBack");
try {
mService.invokCallBack();
} catch (RemoteException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
--mService.invokCallBack()等同調用Proxy.invokCallBack,這個時候是進程間調用,我們看代理方法的實現
public void invokCallBack() throws android.os.RemoteException
{
//構造一個Parcel對象,該對象可在進程間傳輸
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
//DESCRIPTOR = "com.cao.android.demos.binder.aidl.AIDLService",描述了調用哪個Stub對象
_data.writeInterfaceToken(DESCRIPTOR);
//Stub.TRANSACTION_invokCallBack 標識調用Stub中哪個接口方法,mRemote在是構造Proxy對象的參數obj,也就是public void onServiceConnected(ComponentName className, IBinder service)
//中的service參數,它是一個BinderProxy對象,負責傳輸進程間數據。
mRemote.transact(Stub.TRANSACTION_invokCallBack, _data, _reply, 0);
_reply.readException();
}
finally {
_reply.recycle();
_data.recycle();
}
}
--5.BinderProxy.transact 該方法本地化實現
public native boolean transact(int code, Parcel data, Parcel reply,
int flags) throws RemoteException;
//對應實現的本地化代碼 /frameworks/base/core/jni/android_util_Binder.cpp->static jboolean android_os_BinderProxy_transact(JNIEnv* env, jobject obj,
jint code, jobject dataObj,
jobject replyObj, jint flags)
//具體進程通信在c代碼中如何實現,以後再深入研究。
--6.服務端進程數據接收
--調用堆棧
##AIDLService.Stub.onTransact
##AIDLService.Stub(Binder).execTransact
##NativeStart.run
--AIDLService.Stub.onTransact
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_registerTestCall:
{
data.enforceInterface(DESCRIPTOR);
com.cao.android.demos.binder.aidl.AIDLActivity _arg0;
_arg0 = com.cao.android.demos.binder.aidl.AIDLActivity.Stub.asInterface(data.readStrongBinder());
this.registerTestCall(_arg0);
reply.writeNoException();
return true;
}
//TRANSACTION_invokCallBack由前面客戶端調用的時候transact方法參數決定,code==TRANSACTION_invokCallBack,執行
//invokCallBack方法,方法由繼承Stud的服務端存根類實現。
case TRANSACTION_invokCallBack:
{
data.enforceInterface(DESCRIPTOR);
this.invokCallBack();
reply.writeNoException();
return true;
}
5.裡面設置本地C代碼的調用,我沒有深入研究,隨著後面我對android框架的深入,我會發blog進一步說民底層C代碼是如何實現進程通信的,關於AIDL進程通信,暫時研究到這裡。
android app自動化測試之UIAutomator,androiduiautomator一、UIAutomator
Android基礎入門教程——8.3.16 Canvas API詳解(Part 1) Android基礎入門教程——8.3.16 Canvas A
Linux內核系列—6.操作系統開發之內存分頁機制,linux分頁a.概述 頁尺寸是4KB,頁表每個表項占4字節,CR3寄存器給出了頁目錄的物理基地址;頁目錄給出了所有頁
Android中使用開源框架Fresco處理圖片,本文為原創博文,轉載請注明原文鏈接:http://www.cnblogs.com/panhouye/p/6278116.