Có rất nhiều cách có thể giúp các ứng dụng chia sẻ dữ liệu cho nhau. Chúng ta có thể sử dụng Intent, lưu file external storage, có thể sử dụng Content Provider. Tuy nhiên bạn không thể sử dụng bất kỳ cách nào đã kể trên để giao tiếp hay xử lý logic giữa các ứng dụng. Android cung cấp một cách để có thể giao tiếp liên ứng dụng, đó là Android Interface Definition Language - AIDL.
Android Interface Definition Language không phải một loại ngôn ngữ mới trong Android mà là cách cho phép cả client và server (1 ứng dụng đóng vai trò là server cho các ứng dụng khác đóng vai trò là client có thể truy cập tới) có thể giao tiếp với nhau thông qua truyền thông liên tiến trình (Interprocess communication - IPC). Thông thường trong Android, một process (tiến trình) không thể trực tiếp truy cập vào bộ nhớ của một tiến trình khác. Vì vậy để có thể các tiến trình có thể giao tiếp với nhau, chúng cần phân tách các đối tượng thành dạng đối tượng nguyên thủy (primitive object) mà hệ thống có thể hiểu được.
Lưu ý: Chỉ sử dụng AIDL nếu cho phép client từ các ứng dụng khác nhau truy cập vào dịch vụ IPC và muốn xử lý đa luồng trong dịch vụ của bạn. Nếu không cần thực hiện IPC đồng thời trên các ứng dụng khác nhau, bạn nên tạo interface của mình bằng cách triển khai Binder hoặc nếu bạn muốn thực hiện IPC nhưng không cần xử lý đa luồng, hãy triển khai bằng Messenger.
1. Xây dựng AIDL interface
Bạn phải xác định interface AIDL của mình trong tệp .aidl bằng Java, sau đó lưu nó trong thư mục src/ của cả ứng dụng server và bất kỳ ứng dụng client nào khác liên kết với server. Khi bạn tạo trên từng ứng dụng có chứa tệp .aidl, Android SDK tools sẽ tạo interface IBinder dựa trên tệp .aidl và lưu nó trong thư mục generated của dự án. Service phải triển khai interface IBinder nếu thích hợp. Các ứng dụng client sau đó có thể liên kết với Service của ứng dụng và các phương thức gọi từ IBinder để thực hiện IPC.
Để tạo một Service được giới hạn bằng AIDL, hãy làm theo các bước sau:
-
Tạo tệp .aidl trong thư mục src/
-
Implement interface: Android SDK tools tạo interface IBinder bằng Java dựa trên tệp .aidl của bạn. Interface này có một abstract class bên trong có tên là Stub được extend từ Binder và implement các phương thức từ interface AIDL của bạn. Bạn phải extend class Stub đó và implement các phương thức.
-
Hiển thị interface cho client: Implement một Service and override lại function onBind() để trả về việc triển khai lớp Stub của bạn.
Thận trọng: Bất kỳ thay đổi nào bạn thực hiện đối với interface AIDL của mình sau bản phát hành đầu tiên phải vẫn tương thích ngược để tránh phá vỡ các ứng dụng khác sử dụng sevice của bạn.
1.1 Tạo file .aidl
AIDL sử dụng cú pháp rất đơn giản cho phép bạn khai báo một interface với một hoặc nhiều phương thước có thể có một hoặc nhiều tham số và trả về một giá trị. Các tham số và giá trị trả về có thể thuộc bất kì loại nào thậm chí các interface hoặc object được tạo ra bởi các AIDL khác.
Bạn cần xây dựng một .aidl file sử dụng ngôn ngữ Java, mỗi một .aidl file cần định nghĩa duy nhất một interface.
Mặc định AIDL hộ trỡ các kiểu dữ liệu sau:
- 8 kiểu dữ liệu nguyên thủy trong java (int, byte, short, long, float, double, boolean, char)
- String
- CharSequence
- List, Map: Tất cả các phần tử trong List, Map phải là một trong những loại dữ liệu được hỗ trợ trong danh sách này hoặc một trong các đối tượng được định nghĩa thông qua một aidl interface khác.
Khi định nghĩa aidl interface bạn cần lưu ý:
- Phương thức có thể nhận vào 0 hoặc nhiều parameter và trả về void hoặc một giá trị cụ thể
- AIDL chỉ hỗ trợ các phương thức không hỗ trợ các static field
- Tất cả các comment code có trong tệp .aidl sẽ được đưa vào IBinder interface (ngoại trừ các comment trước câu lệnh import và package)
- Các hằng số string và int có thể được định nghĩa trong AIDL interface
- Các tham số Nullable và kiểu trả về có Nullable phải được gắn annotation @nullable.
Đây là một file .aidl cụ thể:
// 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);
}
Chỉ cần lưu file .aidl vào thư mục src/ và khi build, SDK tools sẽ tự generate ra file IBinder interface trong thư mục gen/. File generated này sẽ có tên trùng với tên của file .aidl (ví dụ IRemoteService.aidl thì sẽ generate ra file IRemoteService.java)
1.2 Implement interface
Với file IRemoteService.aidl như ví dụ trước đó, khi build ứng dụng, SDK tools sẽ tạo ra 1 file IRemoteService.java. File IRemoteService.java được gen bao gồm 1 subclass IRemoteService.Stub là abstract class implement từ IRemoteService.aidl và khai báo tất cả các phương thức từ file IRemoteService.aidl.
Lưu ý Subclass IRemoteService.Stub định nghĩa một vài phương thức helper, phương thức quan trọng nhất là asInterFace() với đầu vào là một Binder và trả về một instance của Stub interface. Phần này sẽ hướng dẫn sau đó.
Để implement interface được tạo từ IRemoteService.aidl, tạo ra một object IRemoteService.Stub và implement lại các phương thức được kế thừa từ tệp IRemoteService.aidl.
Dưới đây là ví dụ:
private val binder = object : IRemoteService.Stub() {
override fun getPid(): Int =
Process.myPid()
override fun basicTypes(
anInt: Int,
aLong: Long,
aBoolean: Boolean,
aFloat: Float,
aDouble: Double,
aString: String
) {
// Does nothing
}
}
Bây giờ object binder là một instance của class IRemoteService.Stub (giống như một Binder), định nghĩa giao diện RPC (Remote Procedure Calls - lời gọi thủ tục thông thường trong trường hợp mà caller và receiver không cùng nằm trong một process) cho service. Trong bước tiếp theo, biến này có thể trả về cho các client sử dụng thông qua method public IBinder onBind(Intent intent).
Có một số quy tắc bạn nên biết khi implement AIDL:
- Các phuơng thức đến không được đảm bảo sẽ được thực thi trên main thread, vì vậy bạn cần xử lý multil thread và xây dựng thread-save
- Theo mặc định, các cuộc gọi RPC là tuần tự. Nếu service mất nhiều thời gian để hoàn thành một request, bạn không nên gọi nó từ main thread, vì nó có thể làm xuất hiện lỗi ARN
- Chỉ các loại ngoại lệ được liệt kê trong tài liệu tham khảo cho Parcel.writeException() mới được gửi lại cho client nên gần như sẽ không có bất cứ Exception nào được ném trả lại
1.3 Hiển thị interface cho client
Khi bạn đã implement interface cho Service ở bước 2, bạn cần xuất ra interface này để cho các client có thể bind tới service của bạn. Để hiển thị interface cho client, hãy extend Service và implement onBind() để trả về một instance của class IRemoteService.Stub đã tạo. Đây là một dịch vụ ví dụ hiển thị giao diện mẫu IRemoteService cho client:
class RemoteService : Service() {
override fun onCreate() {
super.onCreate()
}
override fun onBind(intent: Intent): IBinder {
// Return the interface
return binder
}
private val binder = object : IRemoteService.Stub() {
override fun getPid(): Int {
return Process.myPid()
}
override fun basicTypes(
anInt: Int,
aLong: Long,
aBoolean: Boolean,
aFloat: Float,
aDouble: Double,
aString: String
) {
// Does nothing
}
}
}
Bây giờ, khi một ứng dụng client (chẳng hạn như activity) gọi bindService() để kết nối với service này, thì callback onServiceConnected() của client sẽ nhận được object binder do phương thức onBind() của service trả về.
Client cũng phải có quyền truy cập vào class interface, vì vậy nếu client và service nằm trong các ứng dụng riêng biệt, thì ứng dụng của client phải có bản sao của tệp .aidl trong thư mục src/ của nó. Khi client nhận được IBinder trong onServiceConnected(), nó phải gọi IRemoteService.Stub.asInterface(service) để truyền tham số trả về thành kiểu IRemoteService. Ví dụ:
var iRemoteService: IRemoteService? = null
val mConnection = object : ServiceConnection {
// Called when the connection with the service is established
override fun onServiceConnected(className: ComponentName, service: IBinder) {
// Following the example above for an AIDL interface,
// this gets an instance of the IRemoteInterface, which we can use to call on the service
iRemoteService = IRemoteService.Stub.asInterface(service)
}
// Called when the connection with the service disconnects unexpectedly
override fun onServiceDisconnected(className: ComponentName) {
Log.e(TAG, "Service has unexpectedly disconnected")
iRemoteService = null
}
}
2. Truyền các object qua IPC
AIDL chỉ suport một số kiểu dữ liệu nguyên thủy và một số kiểu dữ liệu khác như String, List, Map, CharSequence (xem lại phần 1.1). Bài toán đặt ra là chúng ta muốn truyền một đối tượng thông qua AIDL thì làm như thế nào? Câu trả lời là phân tách đối tượng đó thành các kiểu dữ liệu nguyên thủy và truyền qua client như bình thường.
Các bước cần thực hiện như sau:
- Tạo 1 đối tượng implement Parcelable interface
- Implement method writeToParcel và ghi nhữnng biến cuả object mà bạn muốn truyền thông qua method .writeXXX của Parcel
- Thêm một biến static là CREATOR implement Parcelable.Creator interface.
- Tạo một .aidl interface đại diện cho class đó
import android.os.Parcel
import android.os.Parcelable
class Rect() : Parcelable {
var left: Int = 0
var top: Int = 0
var right: Int = 0
var bottom: Int = 0
companion object CREATOR : Parcelable.Creator<Rect> {
override fun createFromParcel(parcel: Parcel): Rect {
return Rect(parcel)
}
override fun newArray(size: Int): Array<Rect> {
return Array(size) { Rect() }
}
}
private constructor(inParcel: Parcel) : this() {
readFromParcel(inParcel)
}
override fun writeToParcel(outParcel: Parcel, flags: Int) {
outParcel.writeInt(left)
outParcel.writeInt(top)
outParcel.writeInt(right)
outParcel.writeInt(bottom)
}
private fun readFromParcel(inParcel: Parcel) {
left = inParcel.readInt()
top = inParcel.readInt()
right = inParcel.readInt()
bottom = inParcel.readInt()
}
override fun describeContents(): Int {
return 0
}
}
package android.graphics;
// Declare Rect so AIDL can find it and knows that it implements
// the parcelable protocol.
parcelable Rect;
Từ Android 10 (API 29), bạn có thể định nghĩa ra các object Parcelable ngay trong AIDL thay vì phải define rõ ràng class Rect như đoạn code bên trên:
package android.graphics;
// Declare Rect so AIDL can find it and knows that it implements
// the parcelable protocol.
parcelable Rect {
int left;
int top;
int right;
int bottom;
}
Ở client muốn sử dụng bạn chỉ cần define class và aidl tuơng tự là có thể sử dụng được.
3. Các phương thức có đối số truyền vào kiểu Parcelable
Nếu AIDL interface của bạn bao gồm các phương thức chấp nhận các tham số là object parcelable, hãy đảm bảo bạn gọi Bundle.setClassLoader(ClassLoader) trước khi cố gắng đọc từ Bundle. Nếu không, ClassNotFoundException sẽ được ném ra mặc dù Bundle được xác định chính xác trong ứng dụng của bạn.
Ví dụ file .aidl như dưới:
// IRectInsideBundle.aidl
package com.example.android;
/** Example service interface */
interface IRectInsideBundle {
/** Rect parcelable is stored in the bundle with key "rect" */
void saveRect(in Bundle bundle);
}
thì cần khai báo một instance của subclass Stub như dưới:
private val binder = object : IRectInsideBundle.Stub() {
override fun saveRect(bundle: Bundle) {
bundle.classLoader = classLoader
val rect = bundle.getParcelable<Rect>("rect")
process(rect) // Do more with the parcelable
}
}
4. Cách gọi một phương thức IPC
Dưới đây là các bước mà một client class phải thực hiện để gọi một phương thức từ remote interface được xác định bằng AIDL:
- Thêm file .aidl trong thư mục src/ của dự án.
- Khai báo một instance của IBinder interface (được tạo dựa trên AIDL).
- Triển khai ServiceConnection.
- Gọi Context.bindService()
- Trong quá trình implement onServiceConnected(), bạn sẽ nhận được một instance của IBinder (từ phía service được call). Gọi YourInterfaceName.Stub.asInterface ((IBinder) service) để truyền tham số trả về thành kiểu YourInterface.
- Gọi các phương thức mà bạn đã xác định trên interface của mình. Bạn nên luôn bắt DeadObjectException, các ngoại lệ này được ném ra khi kết nối bị đứt đoạn. Bạn cũng nên bắt thêm cả SecurityException, các ngoại lệ này được đưa ra khi hai quy trình liên quan đến lệnh gọi phương thức IPC có các định nghĩa AIDL xung đột.
- Để ngắt kết nối, hãy gọi Context.unbindService() với instance của interface của bạn.
Bên dưới là demo tham khảo:
private const val BUMP_MSG = 1
class Binding : Activity() {
/** The primary interface we will be calling on the service. */
private var mService: IRemoteService? = null
/** Another interface we use on the service. */
internal var secondaryService: ISecondary? = null
private lateinit var killButton: Button
private lateinit var callbackText: TextView
private lateinit var handler: InternalHandler
private var isBound: Boolean = false
/**
* Class for interacting with the main interface of the service.
*/
private val mConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
// 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)
killButton.isEnabled = true
callbackText.text = "Attached."
// We want to monitor the service for as long as we are
// connected to it.
try {
mService?.registerCallback(mCallback)
} catch (e: RemoteException) {
// 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(
this@Binding,
R.string.remote_service_connected,
Toast.LENGTH_SHORT
).show()
}
override fun onServiceDisconnected(className: ComponentName) {
// This is called when the connection with the service has been
// unexpectedly disconnected -- that is, its process crashed.
mService = null
killButton.isEnabled = false
callbackText.text = "Disconnected."
// As part of the sample, tell the user what happened.
Toast.makeText(
this@Binding,
R.string.remote_service_disconnected,
Toast.LENGTH_SHORT
).show()
}
}
/**
* Class for interacting with the secondary interface of the service.
*/
private val secondaryConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
// Connecting to a secondary interface is the same as any
// other interface.
secondaryService = ISecondary.Stub.asInterface(service)
killButton.isEnabled = true
}
override fun onServiceDisconnected(className: ComponentName) {
secondaryService = null
killButton.isEnabled = false
}
}
private val mBindListener = View.OnClickListener {
// 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.
val intent = Intent(this@Binding, RemoteService::class.java)
intent.action = IRemoteService::class.java.name
bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
intent.action = ISecondary::class.java.name
bindService(intent, secondaryConnection, Context.BIND_AUTO_CREATE)
isBound = true
callbackText.text = "Binding."
}
private val unbindListener = View.OnClickListener {
if (isBound) {
// If we have received the service, and hence registered with
// it, then now is the time to unregister.
try {
mService?.unregisterCallback(mCallback)
} catch (e: RemoteException) {
// There is nothing special we need to do if the service
// has crashed.
}
// Detach our existing connection.
unbindService(mConnection)
unbindService(secondaryConnection)
killButton.isEnabled = false
isBound = false
callbackText.text = "Unbinding."
}
}
private val killListener = View.OnClickListener {
// 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.
try {
secondaryService?.pid?.also { pid ->
// 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)
callbackText.text = "Killed service process."
}
} catch (ex: RemoteException) {
// Recover gracefully from the process hosting the
// server dying.
// Just for purposes of the sample, put up a notification.
Toast.makeText(this@Binding, 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 val mCallback = object : 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.
*/
override fun valueChanged(value: Int) {
handler.sendMessage(handler.obtainMessage(BUMP_MSG, value, 0))
}
}
/**
* Standard initialization of this activity. Set up the UI, then wait
* for the user to poke it before doing anything.
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.remote_service_binding)
// Watch for button clicks.
var button: Button = findViewById(R.id.bind)
button.setOnClickListener(mBindListener)
button = findViewById(R.id.unbind)
button.setOnClickListener(unbindListener)
killButton = findViewById(R.id.kill)
killButton.setOnClickListener(killListener)
killButton.isEnabled = false
callbackText = findViewById(R.id.callback)
callbackText.text = "Not attached."
handler = InternalHandler(callbackText)
}
private class InternalHandler(
textView: TextView,
private val weakTextView: WeakReference<TextView> = WeakReference(textView)
) : Handler() {
override fun handleMessage(msg: Message) {
when (msg.what) {
BUMP_MSG -> weakTextView.get()?.text = "Received from service: ${msg.arg1}"
else -> super.handleMessage(msg)
}
}
}
}