English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Spiegazione dettagliata del meccanismo di comunicazione tra processi AIDL in Android

AIDL di Android, meccanismo di comunicazione tra processi di Android, ecco una raccolta di conoscenze su AIDL per aiutare tutti a imparare e comprendere questa parte di conoscenza!

Cos'è AIDL

AIDL, abbreviazione di Android Interface Definition Language, è noto anche come Linguaggio di Descrizione dell'Interfaccia Android. Suona molto complesso, ma in realtà la sua essenza è un strumento ausiliario per la generazione di interfacce di comunicazione tra processi. Esiste sotto forma di file .aidl, e tutto ciò che il sviluppatore deve fare è definire l'interfaccia di comunicazione tra processi in questo file. Durante la compilazione, l'IDE genererà automaticamente i file .java utilizzabili nel progetto in base al nostro file di interfaccia .aidl, il che è simile a quello che chiamiamo "dolcezza sintattica".

La sintassi di AIDL è la stessa della sintassi di java, con alcune differenze minime nella dichiarazione dei pacchetti. In java, se due classi si trovano nello stesso pacchetto, non è necessario eseguire l'operazione di dichiarazione del pacchetto, ma in AIDL, è necessario eseguire la dichiarazione di importazione.

 Dettagli su AIDL

Immaginiamo una scena: abbiamo un sistema di gestione dei libri, questo sistema viene implementato tramite il modello CS. Le funzioni di gestione specifiche vengono implementate dal processo server, il client deve solo chiamare l'interfaccia corrispondente.

Quindi prima definiamo l'interfaccia ADIL di questo sistema di gestione.

Abbiamo creato una nuova directory aidl in /rc, nella directory ci sono tre file Book.java, Book.aidl, IBookManager.aidl.

package com.example.aidl book
public class Book implements Parcelable {
 int bookId;
 String bookName;
 public Book(int bookId, String bookName) {
   this.bookId = bookId;
   this.bookName = bookName;
 }
 ...
}

package com.example.aidl;

Parcelable Book;

package com.example.aidl;
import com.example.aidl.Book;
inteface IBookManager {
  List<Book> getBookList();
  void addBook(in Book book);
}

Di seguito, spieghiamo questi tre file rispettivamente:

Book.java è la classe entità che abbiamo definito, che implements l'interfaccia Parcelable, in modo che la classe Book possa essere trasportata tra processi.
Book.aidl è la dichiarazione di questa classe entità nell'AIDL.
IBookManager è l'interfaccia di comunicazione tra il server e il client. (Attenzione, negli interfacce AIDL, oltre ai tipi di base, è necessario aggiungere una direzione davanti ai parametri, in indica il tipo di parametro in ingresso, out indica il tipo di parametro in uscita, inout indica il tipo di parametro di input/output)

Il compilatore ha compilato, Android Studio ha generato automaticamente un file .java per il nostro progetto, questo file contiene tre classi, queste tre classi sono IBookManager, Stub e Proxy, queste tre classi sono di tipo statico, possiamo separarle completamente, le definizioni delle tre classi sono come segue:

IBookManager

public interface IBookManager extends android.os.IInterface {
  public void addBook(net.bingyan.library.Book book) throws android.os.RemoteException;
  public java.util.List<net.bingyan.library.Book> getBookList() throws android.os.RemoteException;
}

Stub

public static abstract class Stub extends android.os.Binder implements net.bingyan.library.IBookManager {
    private static final java.lang.String DESCRIPTOR = "net.bingyan.library.IBookManager";
    static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
    static final int TRANSACTION_getBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
    /**
     * Construct the stub at attach it to the interface.
     */
    public Stub() {
      this.attachInterface(this, DESCRIPTOR);
    }
    /**
     * Cast an IBinder object into an net.bingyan.library.IBookManager interface,
     * generating a proxy if needed. http://www.manongjc.com/article/1501.html
     */
    public static net.bingyan.library.IBookManager asInterface(android.os.IBinder obj) {
      if ((obj == null)) {
        return null;
      }
      android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
      if (((iin != null) && (iin instanceof net.bingyan.library.IBookManager))) {
        return ((net.bingyan.library.IBookManager) iin);
      }
      return new net.bingyan.library.IBookManager.Stub.Proxy(obj);
    }
    @Override
    public android.os.IBinder asBinder() {
      return this;
    }
    @Override
    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_addBook: {
          data.enforceInterface(DESCRIPTOR);
          net.bingyan.library.Book _arg0;
          if ((0 != data.readInt())) {
            _arg0 = net.bingyan.library.Book.CREATOR.createFromParcel(data);
          } else {
            _arg0 = null;
          }
          this.addBook(_arg0);
          reply.writeNoException();
          return true;
        }
        case TRANSACTION_getBookList: {
          data.enforceInterface(DESCRIPTOR);
          java.util.List<net.bingyan.library.Book> _result = this.getBookList();
          reply.writeNoException();
          reply.writeTypedList(_result);
          return true;
        }
      }
      return super.onTransact(code, data, reply, flags);
    }
}

Proxy

private static class Proxy implements net.bingyan.library.IBookManager {
      private android.os.IBinder mRemote;
      Proxy(android.os.IBinder remote) {
        mRemote = remote;
      }
      @Override
      public android.os.IBinder asBinder() {
        return mRemote;
      }
      public java.lang.String getInterfaceDescriptor() {
        return DESCRIPTOR;
      }
      /**
       * Demonstrates some basic types that you can use as parameters
       * and return values in AIDL.  http://www.manongjc.com/article/1500.html
       */
      @Override
      public void addBook(net.bingyan.library.Book book) throws android.os.RemoteException {
        android.os.Parcel _data = android.os.Parcel.obtain();
        android.os.Parcel _reply = android.os.Parcel.obtain();
        try {
          _data.writeInterfaceToken(DESCRIPTOR);
          if ((book != null)) {
            _data.writeInt(1);
            book.writeToParcel(_data, 0);
          } else {
            _data.writeInt(0);
          }
          mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
          _reply.readException();
        }
          _reply.recycle();
          _data.recycle();
        }
      }
      @Override
      public java.util.List<net.bingyan.library.Book> getBookList() throws android.os.RemoteException {
        android.os.Parcel _data = android.os.Parcel.obtain();
        android.os.Parcel _reply = android.os.Parcel.obtain();
        java.util.List<net.bingyan.library.Book> _result;
        try {
          _data.writeInterfaceToken(DESCRIPTOR);
          mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
          _reply.readException();
          _result = _reply.createTypedArrayList(net.bingyan.library.Book.CREATOR);
        }
          _reply.recycle();
          _data.recycle();
        }
        return _result;
      }
    }

对生成的这三个类的说明如下:

  1. IBookManager 这个类是我们定义的接口,android studio 给它添加了一个父类,让它继承自 android.os.interface 这个接口,这个接口只有一个方法 IBinder asBinder(),这样 IBookManager 中就有三个带实现的方法了,它是服务端进程和客户端进程通信的窗口。
  2. Stub 是一个抽象类,这个类继承自 android.os.Binder 类,并且实现了 IBookManager 这个接口。在 Stub 中,已经实现了 asBinder() 这个接口方法,还有两个是我们定义的 AIDL 接口方法留给继承它的子类去实现。它用在服务端,因此服务端需要实现这两个方法。
  3. Proxy 顾名思义是一个代理类,它是服务端在客户端的一个代理,它也实现了 IBookManager 接口,并且实现了 IBookManager 中的所有方法。它用在客户端,是服务端在客户端的代理。现在我们对这三个类逐个分析:
  4. IBookManager 这个类没什么好说的,它只是简单继承了 asInterface 这个接口,作用就是将 IBookManager 转换成 IBinder。
  5. Proxy 这个类已经提到过,它就是进程间通信机制的一个封装类,它的内部实现机制是 Binder,通过构造方法我们也容易看出。它的构造方法接受一个 IBinder 类型的参数,参数名为 remote,显然,它代表着服务端。我们看看这个类中的方法 addBook() 和 getBookList():
@Override
public void addBook(net.bingyan.library.Book book) throws android.os.RemoteException {
   android.os.Parcel _data = android.os.Parcel.obtain();
   android.os.Parcel _reply = android.os.Parcel.obtain();
   try {
      _data.writeInterfaceToken(DESCRIPTOR)
      if ((book != null)) {
        _data.writeInt(1);
        book.writeToParcel(_data, 0);
      } else {
        _data.writeInt(0);
      }
      mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
      _reply.readException();
    }
      _reply.recycle();
      _data.recycle();
    }
}
@Override /* http://www.manongjc.com/article/1547.html */
public java.util.List<net.bingyan.library.Book> getBookList() throws android.os.RemoteException {
    android.os.Parcel _data = android.os.Parcel.obtain();
    android.os.Parcel _reply = android.os.Parcel.obtain();
    java.util.List<net.bingyan.library.Book> _result;
    try {
       _data.writeInterfaceToken(DESCRIPTOR);
       mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
       _reply.readException();
       _result = _reply.createTypedArrayList(net.bingyan.library.Book.CREATOR);
    }
      _reply.recycle();
      _data.recycle();
    }
    return _result;
}

Sono implementati automaticamente dal compilatore, queste due metodologie condividono molte somiglianze e posso rivelarvi qui: queste due metodologie sono le finestre di chiamata del processo client al processo server. All'inizio di queste due metodologie, entrambe definiscono due oggetti Parcel (nome cinese: imballaggio). La classe Parcel sembra familiare a noi, sì, i parametri writeToParcel() e createFromParcel() della classe Book sono di tipo Parcel, riguardo a questa classe, la documentazione spiega come segue:

Contenitore per un messaggio (dati e riferimenti agli oggetti) che può essere trasmesso attraverso un IBinder. Un Parcel può contenere sia dati appiattiti che verranno ripristinati dall'altro lato dell'IPC (utilizzando i vari metodi qui per scrivere tipi specifici, o l'interfaccia generale {@link Parcelable}), sia riferimenti a oggetti vivi {@link IBinder} che faranno sì che l'altro lato riceva un proxy IBinder connesso con l'oggetto IBinder originale nel Parcel.

Proxy è un contenitore che può trasmettere messaggi tramite IBinder. Un Parcel può contenere dati serializzabili, che verranno deserializzati dall'altro lato dell'IPC; può anche contenere riferimenti a oggetti IBinder, il che farà sì che l'altro lato riceva un'istanza proxy di IBinder connessa con l'oggetto IBinder originale nel Parcel.

Di seguito, utilizziamo le immagini per illustrare chiaramente:

Il meccanismo di comunicazione dei processi in Android tramite AIDL

Come illustrato nella figura, possiamo vedere chiaramente che il server comunica con il client utilizzando Parcel come pacchetto di dati attraverso Binder. Il pacchetto di dati è l'oggetto serializzato.

Come descritto sopra, entrambi i metodi definiscono due oggetti Parcel, rispettivamente chiamati _data e _reply, per parafrasare, dal punto di vista del client, _data è il pacchetto di dati inviato dal client al server, mentre _reply è il pacchetto di dati inviato dal server al client.

Dopo di che si inizia a utilizzare questi due oggetti per comunicare con il server, possiamo osservare che entrambi i metodi contengono una chiamata al metodo mRemote.transact(), che ha quattro parametri, il significato del primo parametro sarà spiegato in seguito, il secondo parametro _data è responsabile dell'invio di pacchetti di dati al server come i parametri del metodo dell'interfaccia, il terzo parametro _reply è responsabile della ricezione di pacchetti di dati dal server come i valori di ritorno del metodo dell'interfaccia. Questa riga di codice è solo una chiamata di metodo semplice, ma è la parte più coreana della comunicazione AIDL, che in realtà esegue una chiamata di metodo remoto (il client chiama il metodo同名 dello Stub del server attraverso l'interfaccia Proxy locale esposta), quindi puoi immaginare che sia un'operazione che richiede molto tempo.

Nel nostro esempio:

1. Il metodo addBook(Book book) ha bisogno di aiuto da parte di _data per inviare il parametro Book:book al server, il modo di invio è imballare Book attraverso il metodo writeToParcel(Parcel out) implementato, come puoi immaginare, _data è in realtà il parametro out, ricordi l'implementazione di questo metodo in Book? Abbiamo imballato i campi di Book uno per uno nel Parcel.

2. Il metodo getBookList() ha bisogno di aiuto da parte di _reply per ricevere il valore di ritorno List<Book>:books dal server, la tecnica utilizzata è quella di passare il campo statico CREATOR di Book come parametro al metodo createTypedArrayList() di _reply. Ricordi il CREATOR di Book? All'epoca ti sei forse chiesto come utilizzare questo campo statico? Ora tutto è chiaro, dobbiamo contare su questo oggetto (che possiamo chiamare 'deserializzatore' per facilitarne la comprensione) per deserializzare i dati del server e rigenerare oggetti o array serializzabili. È chiaro che CREATOR ha utilizzato _reply per generare List<Book>:books.

Certo, questi due metodi non solo trasmettono oggetti, ma trasmettono anche alcune informazioni di convalida, che non dobbiamo approfondire, ma è importante notare che l'ordine di imballaggio e di smistamento del Parcel deve essere rigorosamente corrispondente. Ad esempio, se il primo imballaggio è int:i, il primo smistamento dovrebbe essere anche questo valore intero. Questo significa che se il primo chiamato durante l'imballaggio è Parcel.writeInt(int), durante lo smistamento dovrebbe essere chiamato Parcel.readInt().

Arrivato qui, la spiegazione del Proxy del client è terminata, ora vediamo lo Stub del server.

La classe Stub ha implementato uno dei metodi di IBookManager, questo è molto semplice, è semplicemente restituire se stesso, perché Stub deriva direttamente da Binder, e Binder deriva da IBinder, quindi non ci sono problemi. Potresti chiedere: ci sono ancora due metodi non implementati? Questi due metodi sono i metodi dell'interfaccia definiti da noi, che lasciamo al processo del server per implementare, il che significa che in futuro dobbiamo definire un implementatore di Stub nel processo del server. Di seguito analizziamo due metodi importanti della classe Stub:

IBookManager asInterface(IBinder obj)

public static net.bingyan.library.IBookManager asInterface(android.os.IBinder obj) {
      if ((obj == null)) {
        return null;
      }
      android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
      if (((iin != null) && (iin instanceof net.bingyan.library.IBookManager))) {
        return ((net.bingyan.library.IBookManager) iin);
      }
      return new net.bingyan.library.IBookManager.Stub.Proxy(obj);
    }

Il ruolo di questo metodo è quello di convertire la classe Stub in un'interfaccia IBookManager, nel metodo c'è una condizione: se il processo del server e il processo del client sono lo stesso processo, allora si convertirà direttamente la classe Stub in IBookManager tramite la conversione di tipo; se non sono lo stesso processo, allora si convertirà Stub in IBookManager tramite l'oggetto proxy Proxy. Perché farlo, sappiamo che se il processo del server e il processo del client non sono lo stesso processo, le loro memorie non possono essere condivise e non possono comunicare attraverso il modo generale, ma se noi stessi implementiamo il modo di comunicazione tra processi, il costo per lo sviluppatore comune è troppo alto, quindi il compilatore ci aiuta a generare uno strumento di encapsulamento della comunicazione tra processi, ovvero questo Proxy, questa classe encapsula il meccanismo di comunicazione tra processi di basso livello e simultaneously esponendo solo i metodi dell'interfaccia, il client deve chiamare questi due metodi per realizzare la comunicazione tra processi (che è in realtà la chiamata remota del metodo) senza dover conoscere i dettagli interni.

Con questo metodo, possiamo utilizzare il suo aiuto sui client per convertire una variabile di tipo IBinder in un'interfaccia definita da noi, IBookManager, la cui scena di utilizzo sarà spiegata in esempi successivi.

onTransact(int code, Parcel data, Parcel reply, int flags)

@Override
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_addBook: {
          data.enforceInterface(DESCRIPTOR);
          net.bingyan.library.Book _arg0;
          if ((0 != data.readInt())) {
            _arg0 = net.bingyan.library.Book.CREATOR.createFromParcel(data);
          } else {
            _arg0 = null;
          }
          this.addBook(_arg0);
          reply.writeNoException();
          return true; /* http://www.manongjc.com/article/1499.html */
        }
        case TRANSACTION_getBookList: {
          data.enforceInterface(DESCRIPTOR);
          java.util.List<net.bingyan.library.Book> _result = this.getBookList();
          reply.writeNoException();
          reply.writeTypedList(_result);
          return true;
        }
      }
      return super.onTransact(code, data, reply, flags);
}

Non è familiare anche questo metodo? Ne vediamo uno simile in Proxy, chiamato transact(int, Parcel, Parcel, int), con parametri identici e sono entrambi metodi di Binder, quindi cosa c'è di comune tra loro?

Come ho detto prima, transact() esegue una chiamata remota. Se transact() è l'iniziatore della chiamata remota, onTransact() è la risposta alla chiamata remota. Il processo reale è che il client invia una chiamata remota al metodo, il sistema Android risponde e gestisce questa chiamata tramite codice di basso livello, poi chiama il metodo onTransact() del server, estrae i parametri del pacchetto dati, li consegna al metodo同名o implementato dal server, infine impacchetta e restituisce il valore di ritorno al client.

Occorre notare che onTransact() viene eseguito nel pool di thread Binder del processo server, il che significa che se dobbiamo aggiornare l'UI nel metodo onTransact(), dobbiamo utilizzare un Handler.

Il significato del primo parametro di queste due funzioni è l'identificatore del metodo dell'interfaccia AIDL. Nel Stub, sono definiti due costanti come identificatori per queste due funzioni:

static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
   static final int TRANSACTION_getBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);

Se code == TRANSACTION_addBook, significa che il client ha chiamato addBook(); se code == TRANSACTION_getBookList, il client ha chiamato getBookList(), che poi viene elaborato dal metodo del server corrispondente. Ecco un diagramma che rappresenta l'intero processo di comunicazione:

Il meccanismo di comunicazione dei processi in Android tramite AIDL

Dopo aver compreso l'intero processo di AIDL, passiamo all'applicazione di AIDL nei programmi Android.

 L'uso di AIDL

Credo che tutti dovrebbero essere abbastanza familiarizzati con l'uso di Service. Anche se chiamato 'servizio' e eseguito in background, di default si esegue nella thread principale del processo predefinito. In realtà, far eseguire il Service nel processo predefinito è un po' sfruttare le risorse. Molti servizi di sistema di Android si eseguono in processi separati, disponibili per altre applicazioni, come il servizio di gestione delle finestre. Questo metodo ha il vantaggio di permettere a più applicazioni di condividere lo stesso servizio, risparmiare risorse e facilitare la gestione centralizzata dei client. Tuttavia, è necessario prestare attenzione ai problemi di sicurezza delle thread.

Quindi, procediamo a implementare un semplice sistema di gestione delle biblioteche con architettura CS utilizzando AIDL.

Prima di tutto, definiamo il server:

BookManagerService

public class BookManagerService extends Service {
  private final List<Book> mLibrary = new ArrayList<>();
  private IBookManager mBookManager = new IBookManager.Stub() {
    @Override
    public void addBook(Book book) throws RemoteException {
      synchronized (mLibrary) {
        mLibrary.add(book);
        Log.d("BookManagerService", "ora la nostra biblioteca ha " + mLibrary.size() + " libri");
      }
    }
    @Override
    public List<Book> getBookList() throws RemoteException {
      return mLibrary;
    }
  };
  @Override /* http://www.manongjc.com/article/1496.html */
  public IBinder onBind(Intent intent) {
    return mBookManager.asBinder();
  }
}
<service
   android:process=":remote"
   android:name=".BookManagerService"/>

Sul lato server, abbiamo definito la classe BookManagerService, al cui interno abbiamo creato l'oggetto Stub del server e abbiamo implementato i due metodi dell'interfaccia AIDL necessari per definire la strategia di gestione dei libri del server. Nel metodo onBind(), restituiamo l'oggetto IBookManager come IBinder. Sappiamo che quando si binda un servizio, il sistema chiama il metodo onBinder() per ottenere l'oggetto IBinder del server e lo trasforma in un oggetto IBinder del client e lo passa al client, anche se l'IBinder del server e l'IBinder del client sono due oggetti IBinder, ma sono lo stesso oggetto in basso. Quando registriamo il Service in xml, gli assegniamo un nome di processo, in modo che il Service possa eseguire in un processo separato.

Guarda ora l'implementazione del client:}

Client

public class Client extends AppCompatActivity {
  private TextView textView;
  private IBookManager bookManager;
  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.library_book_manager_system_client);
    Intent i = new Intent(Client.this, BookManagerService.class);
    bindService(i, conn, BIND_AUTO_CREATE);
    Button addABook = (Button) findViewById(R.id.button);
    addABook.setOnClickListener(v -> {
      if (bookManager == null) return;
      try {
        bookManager.addBook(new Book(0, "book"));
        textView.setText(getString(R.string.book_management_system_book_count, String.valueOf(bookManager.getBookList().size())));
      } catch (RemoteException e) {
        e.printStackTrace();
      }
    });
    textView = (TextView) findViewById(R.id.textView);
  }
  private ServiceConnection conn = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
      Log.d("Client -->", service.toString());
      bookManager = IBookManager.Stub.asInterface(service);
    }
    @Override
    public void onServiceDisconnected(ComponentName name) {
      Log.d("Client", name.toString());
    }
  };
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical" android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:weightSum="1"
  android:gravity="center">
  <Button
    android:text="http://www.manongjc.com/article/1495.html"
    android:layout_width="111dp"
    android:layout_height="wrap_content"
    android:id="@+id/button" />
  <TextView
    android:layout_marginTop="10dp"
    android:text="@string/book_management_system_book_count"
    android:layout_width="231dp"
    android:gravity="center"
    android:layout_height="wrap_content"
    android:id="@+id/textView" />
</LinearLayout>

Il nostro client è un Activity, nella funzione onCreate() viene effettuata la binding del servizio, nel metodo bindService() c'è un parametro ServiceConnection:conn, poiché la binding del servizio è eseguita in modo asincrono, il ruolo di questo parametro è di essere chiamato di ritorno quando la binding del servizio è riuscita, ha due metodi di callback: uno è chiamato quando la connessione al servizio è riuscita, l'altro è chiamato quando la connessione con il server è interrotta. Oggi ci interessiamo principalmente al metodo onServiceConnected(), qui abbiamo fatto una sola cosa: convertire l'oggetto IBinder passato dal server in un'interfaccia AIDL, abbiamo definito il campo IBookManager:bookManager per mantenere il riferimento. In questo modo, possiamo effettuare chiamate remote dei metodi tramite questo bookManager. Abbiamo registrato un evento sul pulsante del client: ogni volta che viene cliccato, viene aggiunto un libro al server e viene visualizzato il numero di libri attualmente presenti nella biblioteca.

Ora vediamo l'effetto di esecuzione del programma:

Ogni volta che clicchiamo sul pulsante, aggiungiamo con successo un libro al server, il che significa che abbiamo avuto successo nella comunicazione inter-processuale tramite AIDL.

Grazie per la lettura, speriamo di essere utili a tutti, grazie per il supporto del nostro sito!

Dichiarazione: il contenuto di questo articolo è stato tratto da Internet, il copyright spetta agli autori, il contenuto è stato contribuito e caricato autonomamente dagli utenti di Internet, questo sito non possiede il diritto di proprietà, non è stato editato manualmente e non assume alcuna responsabilità legale. Se trovi contenuti sospetti di violazione del copyright, ti preghiamo di inviare una e-mail a: notice#oldtoolbag.com (al momento dell'invio dell'e-mail, sostituisci # con @) per segnalare, fornendo prove pertinenti. Una volta verificata, questo sito eliminerà immediatamente il contenuto sospetto di violazione del copyright.

Ti potrebbe interessare