Ở phần trước mình đã giới thiệu về IPC và sử dụng IPC thông qua Messenger.
Phần 1: https://magz.techover.io/2021/12/19/inter-process-communication-with-android-phan-1/
Còn bây giờ mình sẽ sự dung IPC thông qua AIDL.
Content
- AIDL là gì?
- Tạo .aidl
- Demo với aidl
- Kết luận
1. AIDL là gì?
AIDL hay Android Interface Definition Language là một cách cho phép bạn có thể định nghĩa một cách mà 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ếng 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ỉ nên sử AIDL nếu bạn muốn các ứng dụng khác có thể kết nối tới service của ứng dụng bạn thông qua truyền thông liên tiến trinh. Còn nếu bạn chỉ đơn thuần muốn sử dụng service trong ứng dụng của mình bạn nên sử dụng Binder.
2. Tạo .aidl file
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 phuơ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 ý:
- Phuơng thưsc 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ạo file aidl các bạn sử dụng AS làm như sau:
Đây là một tệp .aidl ví dụ:
// 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);
}
Lưu ý sau khi viết xong file aidl các bạn cần rebuild lại project để SDK tool gen ra file java để có thể sử dụng.
3. Demo với AIDL
Client
Tạo tệp .aidl:
// IIPCExample.aidl
package com.pmirkelam.ipcserver;
// Declare any non-default types here with import statements
interface IIPCExample {
/** Request the process ID of this service */
int getPid();
/** Count of received connection requests from clients */
int getConnectionCount();
/** Set displayed value of screen */
void setDisplayedValue(String packageName, int pid, String data);
}
fragment_aidl.xml:
<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:gravity="center_horizontal"
android:orientation="vertical"
tools:context=".ui.MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:text="@string/say_something_to_server"
android:textSize="25sp" />
<EditText
android:id="@+id/edt_client_data"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="@string/salut"
android:inputType="text"
android:textAlignment="center"
android:textSize="35sp" />
<Button
android:id="@+id/btn_connect"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:text="@string/connect"
android:textSize="18sp" />
<LinearLayout
android:id="@+id/linear_layout_client_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:visibility="invisible">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:text="@string/process_id_of_server"
android:textSize="25sp" />
<TextView
android:id="@+id/txt_server_pid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="50sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="@string/connection_count_of_server"
android:textSize="25sp" />
<TextView
android:id="@+id/txt_server_connection_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="50sp" />
</LinearLayout>
</LinearLayout>
AIDLFragmet:
class AidlFragment : Fragment(), ServiceConnection, View.OnClickListener {
private var _binding: FragmentAidlBinding? = null
private val binding get() = _binding!!
var iRemoteService: IIPCExample? = null
private var connected = false
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentAidlBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
binding.btnConnect.setOnClickListener(this)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onClick(v: View?) {
connected = if (connected) {
disconnectToRemoteService()
binding.txtServerPid.text = ""
binding.txtServerConnectionCount.text = ""
binding.btnConnect.text = getString(R.string.connect)
binding.linearLayoutClientInfo.visibility = View.INVISIBLE
false
} else {
connectToRemoteService()
binding.linearLayoutClientInfo.visibility = View.VISIBLE
binding.btnConnect.text = getString(R.string.disconnect)
true
}
}
private fun connectToRemoteService() {
val intent = Intent("aidlexample")
val pack = IIPCExample::class.java.`package`
pack?.let {
intent.setPackage(pack.name)
activity?.applicationContext?.bindService(
intent, this, Context.BIND_AUTO_CREATE
)
}
}
private fun disconnectToRemoteService() {
if(connected){
activity?.applicationContext?.unbindService(this)
}
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
// Gets an instance of the AIDL interface named IIPCExample,
// which we can use to call on the service
iRemoteService = IIPCExample.Stub.asInterface(service)
binding.txtServerPid.text = iRemoteService?.pid.toString()
binding.txtServerConnectionCount.text = iRemoteService?.connectionCount.toString()
iRemoteService?.setDisplayedValue(
context?.packageName,
Process.myPid(),
binding.edtClientData.text.toString())
connected = true
}
override fun onServiceDisconnected(name: ComponentName?) {
Toast.makeText(context, "IPC server has disconnected unexpectedly", Toast.LENGTH_LONG).show()
iRemoteService = null
connected = false
}
}
main_activity.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="?attr/actionBarSize">
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/nav_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/mobile_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
Server
Tạo 1 tệp .aidl giống Client:
activity_main.xml:
<?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:gravity="center"
android:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:id="@+id/connection_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/no_connected_client"
android:textSize="30sp"
android:textStyle="bold" />
<LinearLayout
android:id="@+id/linear_layout_client_state"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="75dp"
android:text="@string/package_name"
android:textSize="25sp" />
<TextView
android:id="@+id/txt_package_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="30sp"
android:textStyle="bold"
tools:text="com.someone.something" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:text="@string/pid"
android:textSize="25sp" />
<TextView
android:id="@+id/txt_server_pid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="30sp"
android:textStyle="bold"
tools:text="21282" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:text="@string/data"
android:textSize="25sp" />
<TextView
android:id="@+id/txt_data"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="30sp"
android:textStyle="bold"
tools:text="Salut!" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:text="@string/ipc_method"
android:textSize="25sp" />
<TextView
android:id="@+id/txt_ipc_method"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="30sp"
android:textStyle="bold"
tools:text="Something" />
</LinearLayout>
</LinearLayout>
Client.kt:
data class Client(
var clientPackageName: String?,
var clientProcessId: String?,
var clientData: String?,
var ipcMethod: String
)
object RecentClient {
var client: Client? = null
}
- Tạo lớp dữ liệu khách hàng.
- Tạo một lớp RecentClient singleton nơi chúng tôi sẽ giữ máy khách được kết nối cuối cùng.
Constant.kt:
Đây là dữ liệu dùng chung.
// Bundle keys
const val PID = "pid"
const val CONNECTION_COUNT = "connection_count"
const val PACKAGE_NAME = "package_name"
const val DATA = "data"
AidlFragment.kt:
class IPCServerService : Service() {
companion object {
// How many connection requests have been received since the service started
var connectionCount: Int = 0
// Client might have sent an empty data
const val NOT_SENT = "Not sent!"
}
// AIDL IPC - Binder object to pass to the client
private val aidlBinder = object : IIPCExample.Stub() {
override fun getPid(): Int = Process.myPid()
override fun getConnectionCount(): Int = IPCServerService.connectionCount
override fun setDisplayedValue(packageName: String?, pid: Int, data: String?) {
val clientData =
if (data == null || TextUtils.isEmpty(data)) NOT_SENT
else data
// Get message from client. Save recent connected client info.
RecentClient.client = Client(
packageName ?: NOT_SENT,
pid.toString(),
clientData,
"AIDL"
)
}
}
// Pass the binder object to clients so they can communicate with this service
override fun onBind(intent: Intent?): IBinder? {
connectionCount++
// Choose which binder we need to return based on the type of IPC the client makes
return when (intent?.action) {
"aidlexample" -> aidlBinder
else -> null
}
}
// A client has unbound from the service
override fun onUnbind(intent: Intent?): Boolean {
RecentClient.client = null
return super.onUnbind(intent)
}
}
- Tạo đối tượng chất kết dính của chúng tôi bằng cách mở rộng từ lớp Stub. Ở đây chúng tôi điền các phương thức.
- Trả lại đối tượng kết dính của chúng tôi cho khách hàng với ràng buộc.
- Cập nhật đối tượng RecentClient của chúng tôi khi máy khách liên kết và hủy liên kết để giao diện người dùng được cập nhật.
Trong file Manifest:
<service
android:name=".IPCServerService">
<intent-filter>
<action android:name="aidlexample" />
</intent-filter>
</service>
MainActivity.kt:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
override fun onStart() {
super.onStart()
val client = RecentClient.client
binding.connectionStatus.text =
if (client == null) {
binding.linearLayoutClientState.visibility = View.INVISIBLE
getString(R.string.no_connected_client)
} else {
binding.linearLayoutClientState.visibility = View.VISIBLE
getString(R.string.last_connected_client_info)
}
binding.txtPackageName.text = client?.clientPackageName
binding.txtServerPid.text = client?.clientProcessId
binding.txtData.text = client?.clientData
binding.txtIpcMethod.text = client?.ipcMethod
}
}
- Bây giờ chúng tôi có thể cập nhật Hoạt động theo thông tin khách hàng.
- Máy khách được kết nối cuối cùng sẽ được hiển thị. Bố cục sẽ bị ẩn nếu không có ứng dụng khách được kết nối.
Chú ý: Hầu hết các ứng dụng không nên sử dụng AIDL để tạo một bound service, bởi vì nó có thể yêu cầu khả năng đa luồng và có thể dẫn đến việc triển khai phức tạp hơn. Như vậy, AIDL không phù hợp với hầu hết các ứng dụng nên ta sẽ không thảo luận về cách sử dụng nó.
4. Kết Luận
Trên Đây là cách đơn giản nhất để thực hiện IPC bằng AIDL.
Cảm ơn mọi người đã đọc bài viết.