Android Programming: Pushing the Limits -- Chapter 7:Android IPC -- AIDL
服务端:
最终项目结构:
这个项目中,我们将用到自定义类CustomData作为服务端与客户端传递的数据。
Step 1:创建CustomData类
package com.ldb.android.example.aidl; import android.os.Parcel; import android.os.Parcelable; import android.util.Log; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * Created by lsp on 2016/9/1. */ public class CustomData implements Parcelable { private static final String TAG = "CustomData"; private String mName; private List<String> mReference; private Date mCreated; public CustomData(){ mName = ""; mReference = new ArrayList<>(); mCreated = new Date(); } public String getName() { return mName; } public void setName(String name) { mName = name; } public List<String> getReference() { return mReference; } public void setReference(List<String> reference) { mReference = reference; } public Date getCreated() { return mCreated; } public void setCreated(Date created) { mCreated = created; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(mName); dest.writeStringList(mReference); dest.writeLong(mCreated.getTime()); } @Override public boolean equals(Object o) { if(this == o) return true; if(o == null || getClass() != o.getClass()) return false; CustomData that = (CustomData) o; return mCreated.equals(that.mCreated) && mName.equals(that.mName); } @Override public int hashCode() { int result = mName.hashCode(); result = 31 * result + mCreated.hashCode(); return result; } public static final Parcelable.Creator<CustomData> CREATOR = new Parcelable.Creator<CustomData>(){ @Override public CustomData createFromParcel(Parcel source) { CustomData customData = new CustomData(); customData.mName = source.readString(); // customData.mReference = new ArrayList<>(); source.readStringList(customData.mReference); Long created = source.readLong(); Log.d(TAG, "createFromParcel " + created); customData.mCreated = new Date(created); return customData; } @Override public CustomData[] newArray(int size) { return new CustomData[size]; } }; }
为了实现进程间传递,CustomData 需要实现接口Parcelable,writeToParcel()方法和CREATOR是不可少的。
Step 2:创建CustomData类对应的aidl文件, 不过aidl文件先任意命名,不能是CustomData,否则Android Studio不让继续执行。创建完之后再对aidl重命名为CustomData.aidl。注意此aidl文件的package与CustomData的package要保持一致。模块名app上右键-->new-->AIDL,生成文件后重命名,然后修改文件内容为:
// CustomData.aidl package com.ldb.android.example.aidl; parcelable CustomData;
Step 3:继续生成AidlCallback.aidl文件和ApiInterfaceV1.aidl文件,修改文件内容为:
AidlCallback.aidl:
// AidlCallback.aidl package com.ldb.android.example.aidl; // Declare any non-default types here with import statements import com.ldb.android.example.aidl.CustomData; oneway interface AidlCallback { void onDataUpdated(in CustomData[] data); }
ApiInterfaceV1.aidl:
// ApiInterfaceV1.aidl package com.ldb.android.example.aidl; // Declare any non-default types here with import statements import com.ldb.android.example.aidl.CustomData; import com.ldb.android.example.aidl.AidlCallback; interface ApiInterfaceV1 { boolean isPrime(long value); void getAllDataSince(long timestamp, out CustomData[] result); void storeData(in CustomData data); void setCallback(in AidlCallback callback); }
Step 4:菜单 Build --> Make Project 或者 Rebuild Project,如果顺利的话,就能够自动生成AidlCallback.aidl文件和ApiInterfaceV1.aidl文件对应的.java文件。
在编译目录下,如我的目录是AidlService\app\build\generated\source\aidl\... 下有AidlCallback.java和ApiInterfaceV1.java两个文件。或者在创建服务的时候再进行验证。
Step 5:创建服务类AidlService:
package com.ldb.android.example.aidlservice; import android.app.Service; import android.content.Intent; import android.os.IBinder; import android.os.RemoteException; import android.support.annotation.Nullable; import android.util.Log; import com.ldb.android.example.aidl.AidlCallback; import com.ldb.android.example.aidl.ApiInterfaceV1; import com.ldb.android.example.aidl.CustomData; import java.util.ArrayList; import java.util.Date; /** * Created by lsp on 2016/9/1. */ public class AidlService extends Service { private static final String TAG = "AidlService"; private ArrayList<CustomData> mCustomDataCollection; private AidlCallback mCallback; @Override public void onCreate() { super.onCreate(); mCustomDataCollection = new ArrayList<>(); // TODO Populate the list with stored value... } @Nullable @Override public IBinder onBind(Intent intent) { return mBinder; } @Override public boolean onUnbind(Intent intent) { Log.d(TAG, "onUnbind"); return super.onUnbind(intent); } @Override public void onDestroy() { super.onDestroy(); Log.d(TAG, "onDestroy"); } private static boolean isPrimeImpl(long number) { // Implementation left out for brevity... return false; } private void getDataSinceImpl(CustomData[] result, Date since) { int size = mCustomDataCollection.size(); Log.d(TAG, "getDataSinceImpl size = " + size); Log.d(TAG, "since: " + since); int pos = 0; for (int i = 0; i < size && pos < result.length; i++) { CustomData storedValue = mCustomDataCollection.get(i); Log.d(TAG, "storedValue " + i + ": " + storedValue.getCreated()); if (since.before(storedValue.getCreated())) { Log.d(TAG, "add " + i); result[pos++] = storedValue; } } } private void storeDataImpl(CustomData data) { int size = mCustomDataCollection.size(); try { Thread.sleep(30000); } catch (InterruptedException e) { e.printStackTrace(); } for (int i = 0; i < size; i++) { CustomData customData = mCustomDataCollection.get(i); if (customData.equals(data)) { mCustomDataCollection.set(i, data); return; } } mCustomDataCollection.add(data); } private final ApiInterfaceV1.Stub mBinder = new ApiInterfaceV1.Stub() { @Override public boolean isPrime(long value) throws RemoteException { return isPrimeImpl(value); } @Override public void getAllDataSince(long timestamp, CustomData[] result) throws RemoteException { getDataSinceImpl(result, new Date(timestamp)); } @Override public void storeData(CustomData data) throws RemoteException { Log.d(TAG, data.getName() + " -- " + data.getCreated()); storeDataImpl(data); if(mCallback != null){ mCallback.onDataUpdated(new CustomData[]{data}); } } @Override public void setCallback(AidlCallback callback) throws RemoteException { mCallback = callback; mCallback.asBinder().linkToDeath(new DeathRecipient() { @Override public void binderDied() { Log.d(TAG, "binderDied"); mCallback = null; } }, 0); } }; }
服务类中AidlCallback 和 ApiInterfaceV1 分别对应上一步的AidlCallback.java和ApiInterfaceV1.java,与客户端进行通信的就是mBinder,mBinder继承了ApiInterfaceV1.Stub,ApiInterfaceV1.Stub是上一步自动生成的一个类, 查看它的代码,ApiInterfaceV1.Stub实际就是一个Binder,同时它实现了接口ApiInterfaceV1,但没有实现ApiInterfaceV1具体的方法,因此它还是个抽象类,具体实现就得由我们在服务类中完成。而Binder在服务端正是通过onTransact(...)这个方法进行接收客户端的调用的(客户端则是调用transact(...)方法)。
因此服务端要完成的操作是:
1、定义Aidl文件。
2、IDE自动生成Aidl文件对应的java文件。
3、在服务类中定义一个成员变量,这个成员变量是上一步java文件中生成的Stub的一个实例,并且由我们实现Aidl文件中定义的接口方法。
4、在onBind()方法中返回此成员变量。
5、在AndroidManifest.xml文件中声明服务,并且在<inten-filter>中定义<action android.name="..." />,这样客户端可通过此action定位此服务。
客户端:
最终项目结构:
运行效果,三个按钮对应服务的三个方法。
Step 1:将服务端的Aidl文件和CustomData.java文件拷贝到客户端,注意保持package与服务端一致。
Step 2:菜单 Build --> Make Project 或者 Rebuild Project,如果顺利的话,就能够自动生成AidlCallback.aidl文件和ApiInterfaceV1.aidl文件对应的.java文件。
Step 3:实现回调接口AidlCallback.Stub,并定义一个此实现的变量作为客户端成员变量,用于给服务端设置回调。
// Implement the callback mAidlCallback = new AidlCallback.Stub() { @Override public void onDataUpdated(final CustomData[] data) throws RemoteException { Log.d(TAG, data[0].getName() + " was updated"); runOnUiThread(new Runnable() { @Override public void run() { Log.d(TAG, data[0].getName() + " was updated"); Toast.makeText(MainActivity.this, data[0].getName() + " was updated", Toast.LENGTH_SHORT).show(); } }); } };
Step 4:实现接口ServiceConnection,这步是使用Binder进行服务通信必须做的一件事,因为服务端onBind()传出的Binder,最终作为onServiceConnected(ComponentName name, IBinder service)的参数传到客户端。在此方法的实现中,通过ApiInterfaceV1.Stub.asInterface(service)可得到服务端的代理对象。
@Override public void onServiceConnected(ComponentName name, IBinder service) { mService = ApiInterfaceV1.Stub.asInterface(service); try { mService.setCallback(mAidlCallback); } catch (RemoteException e) { e.printStackTrace(); } }
Step 5:bindService,通过Intent并指定action(与服务端设置的保存一致),来实现绑定,不过从Android 5.0(Lollipop)开始需要显示Intent才能完成bindService。
// Since Android 5.0(Lollipop), bindService should use explicit intent. Intent intent = new Intent("com.ldb.android.example.aidlservice.AidlService"); bindService( new Intent(createExplicitFromImplicitIntent(this, intent)), this, BIND_AUTO_CREATE);
Step 6:unbindService。
以上是实现客户端与服务端进行通信的基本步骤。
客户端实例代码:
package com.ldb.android.example.aidlclient; 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.Button; import android.widget.EditText; import android.widget.Toast; import com.ldb.android.example.aidl.AidlCallback; import com.ldb.android.example.aidl.ApiInterfaceV1; import com.ldb.android.example.aidl.CustomData; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; public class MainActivity extends AppCompatActivity implements ServiceConnection{ private static final String TAG = "MainActivity"; private ApiInterfaceV1 mService; private EditText mNumber; private Button mPrime; private Button mStore; private Button mGet; private AidlCallback.Stub mAidlCallback; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mNumber = (EditText) findViewById(R.id.number_input); mPrime = (Button) findViewById(R.id.prime); mStore = (Button) findViewById(R.id.store); mGet = (Button) findViewById(R.id.get); mPrime.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { onCheckForPrime(); } }); mStore.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { final CustomData customData = new CustomData(); String name = mNumber.getText().toString(); customData.setName(name); customData.getReference().add(name + "1"); customData.getReference().add(name + "2"); customData.getReference().add(name + "3"); // customData.setCreated(new GregorianCalendar(2016, 9, 1, 9, 0 ).getTime()); // try { new Thread(new Runnable() { @Override public void run() { try { mService.storeData(customData); Log.d(TAG, "mService.storeData1"); } catch (RemoteException e) { e.printStackTrace(); } } }).start(); Log.d(TAG, "mService.storeData2"); // } catch (RemoteException e) { // e.printStackTrace(); // } } }); mGet.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { CustomData[] result = new CustomData[10]; Date since = new GregorianCalendar(2016, 8, 1, 8, 0 ).getTime(); try { mService.getAllDataSince(since.getTime(), result); Log.d(TAG, "Result: " + result.length); for(int i = 0; i < result.length; i++){ CustomData customData = result[i]; if(customData != null) { Log.d(TAG, result[i].getName() + result[i].getCreated().toString()); for (String s : result[i].getReference()) { Log.d(TAG, " -- " + s); } } } } catch (RemoteException e) { e.printStackTrace(); } } }); } @Override protected void onResume() { super.onResume(); // Since Android 5.0(Lollipop), bindService should use explicit intent. Intent intent = new Intent("com.ldb.android.example.aidlservice.AidlService"); bindService( new Intent(createExplicitFromImplicitIntent(this, intent)), this, BIND_AUTO_CREATE); // Implement the callback mAidlCallback = new AidlCallback.Stub() { @Override public void onDataUpdated(final CustomData[] data) throws RemoteException { Log.d(TAG, data[0].getName() + " was updated"); runOnUiThread(new Runnable() { @Override public void run() { Log.d(TAG, data[0].getName() + " was updated"); Toast.makeText(MainActivity.this, data[0].getName() + " was updated", Toast.LENGTH_SHORT).show(); } }); } }; } @Override protected void onPause() { super.onPause(); unbindService(this); } @Override public void onServiceConnected(ComponentName name, IBinder service) { mService = ApiInterfaceV1.Stub.asInterface(service); try { mService.setCallback(mAidlCallback); } catch (RemoteException e) { e.printStackTrace(); } } @Override public void onServiceDisconnected(ComponentName name) { mService = null; } public void onCheckForPrime() { long number = Long.valueOf(mNumber.getText().toString()); boolean isPrime = false; try { isPrime = mService.isPrime(number); } catch (RemoteException e) { e.printStackTrace(); } String message = isPrime ? "number_is_prime" : "number_not_prime"; Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); } public static Intent createExplicitFromImplicitIntent(Context context, Intent implicitIntent) { // Retrieve all services that can match the given intent PackageManager pm = context.getPackageManager(); List<ResolveInfo> 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; } }
布局文件:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <EditText android:id="@+id/number_input" android:layout_width="match_parent" android:layout_height="wrap_content"/> <Button android:id="@+id/prime" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="prime"/> <Button android:id="@+id/store" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="store"/> <Button android:id="@+id/get" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="get"/> </LinearLayout>