接上一篇 IPC 机制
3.4、使用 AIDL
前面介绍了使用 Messenger 来进行进程间通信的方法,可以发现,Messenger 是以串行的方式处理客户端发来的消息,如果大量的消息同时发送到服务端,服务端仍然只能一个个处理,如果有大量的并发请求,那么用 Messenger 就不合适了。
同时,Messenger 的作用主要是为了传递消息,但是我们可以使用 AIDL 来实现跨进程的方法调用。下面我们使用 AIDL 来进行进程间通信,分为服务端和客户端。首先还是要准备一个实现了 Parcelable 接口的实体类。
public class Book implements Parcelable {
public int bookId;
public String bookName;
public Book(int bookId, String bookName) {
this.bookId = bookId;
this.bookName = bookName;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.bookId);
dest.writeString(this.bookName);
}
protected Book(Parcel in) {
this.bookId = in.readInt();
this.bookName = in.readString();
}
public static final Parcelable.Creator<Book> CREATOR = new Parcelable.Creator<Book>() {
@Override
public Book createFromParcel(Parcel source) {
return new Book(source);
}
@Override
public Book[] newArray(int size) {
return new Book[size];
}
};
@Override
public String toString() {
return "Book{" +
"bookId=" + bookId +
", bookName='" + bookName + '\'' +
'}';
}
}
一、服务端
服务端首先要创建一个 Service 用来监听客户端的连接请求,然后创建一个 AIDL 文件,将需要暴露给客户端调用的接口方法在这个 AIDL 文件中声明,最后在 Service 中实现 AIDL 中声明的接口方法。如果 IBookManager.Stub 类找不到,需要在创建 AIDL 接口后先编译一次。这里采用了 CopyOnWriteArrayList,这个 CopyOnWriteArrayList 支持并发读/写,AIDL 方法是在服务端的 Binder 线程池中执行的,因此当多个客户端同时连接的时候,会存在多个线程同时访问的情形,所以我们要在 AIDL 方法中处理线程同步,这里直接使用 CopyOnWriteArrayList 来进行自动的线程同步。
AIDL 中所支持的是抽象的 List,List 只是一个接口,在这里服务端虽然返回的是 CopyOnWriteArrayList,但是在 Binder 中会按照 List 的规范去访问数据并最终形成一个新的 ArrayList 传递给客户端,和此类似的还有 ConcurrentHashMap。
public class BookManagerService extends Service {
private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<>();
private Binder mBinder = new IBookManager.Stub() {
@Override
public List<Book> getBookList() throws RemoteException {
return mBookList;
}
@Override
public void addBook(Book book) throws RemoteException {
mBookList.add(book);
}
};
@Override
public void onCreate() {
super.onCreate();
mBookList.add(new Book(1, "Android"));
mBookList.add(new Book(2, "IOS"));
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}
在 AndroidManifest 注册 Service,指定了单独的线程。
<service
android:name=".BookManagerService"
android:process=":remote" />
二、客户端
客户端只需要绑定服务端的 Service,绑定成功后,将服务端返回的 IBinder 对象转成 AIDL 接口所属的类型,接着就可以调用 AIDL 中的方法了。绑定成功后,通过 bookManager 调用 getBookList 方法,然后打印所获取的信息。需要注意的是,服务端的方法可能很久才能执行完毕,这时将会导致 ANR,后面会介绍,这里这样写是为了方便了解 AIDL 的实现步骤。
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
IBookManager bookManager = IBookManager.Stub.asInterface(service);
try {
List<Book> list = bookManager.getBookList();
// 这里可以自行查阅 getCanonicalName() 和 getName() 的区别
Log.d(TAG, "query book list,list type:" + list.getClass().getCanonicalName());
Log.d(TAG, "query book list:" + list.toString());
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent intent = new Intent(this, BookManagerService.class);
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onDestroy() {
unbindService(mConnection);
super.onDestroy();
}
}
三、AIDL 文件
package com.mryuan.learndemo;
parcelable Book;
package com.mryuan.learndemo;
import com.mryuan.learndemo.Book;
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
}
在 AIDL 文件中,并不是所有的数据类型都是可以使用的,AIDL 支持 6 种数据类型,其中 Parcelable 对象和 AIDL 对象必须要显示 import 进来,不管它们是否和当前的 AIDL 文件位于同一个包内。
- 基本数据类型(int、long、char、boolean、double 等)。
- String 和 CharSequence。
- List:只支持 ArrayList,里面每个元素都必须能够被 AIDL 支持。
- Map:只支持 HashMap,里面的每个元素都必须被 AIDL 支持,包括 key 和 value。
- Parcelable:所有实现了 Parcelable 接口的对象。
- AIDL:所有的 AIDL 接口本身也可以在 AIDL 文件中声明。
如果 AIDL 文件中用到了自定义的 Parcelable 对象,那么必须新建一个和它同名的 AIDL 文件,并在其中声明它为 Parcelable 类型。比如上面在 IBookManager.aidl 中用到了 Book 这个类,就必须创建相应的 Book.aidl 文件。
在 AIDL 中,除了基本数据类型,其他类型的参数必须标上方向:in、out 或 inout,in表示输入型参数,out 表示输出型参数,inout 表示输入输出型参数。三者的区别需要另外去查阅,总之我们要根据实际需要去指定参数类型,不能一概使用 out 或者 inout,因为这在底层实现是有开销的。另外,AIDL 接口中只支持方法,不支持声明静态常量,这一点区别于传统接口。
最后运行程序,可以看到,虽然我们在服务端返回的是 CopyOnWriteArrayList 类型,但是客户端收到的仍然是 ArrayList 类型。
接下来试着调用另外一个接口方法 addBook,我们在客户端给服务端添加一本书,之后再获取一次。
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
IBookManager bookManager = IBookManager.Stub.asInterface(service);
try {
List<Book> list = bookManager.getBookList();
Log.d(TAG, "query book list:" + list.toString());
bookManager.addBook(new Book(3, "Android 开发艺术探索"));
List<Book> newList = bookManager.getBookList();
Log.d(TAG, "query book list:" + newList.toString());
} catch (RemoteException e) {
e.printStackTrace();
}
}
运行程序,可以看到添加成功了。
AIDL 观察者模式
假设现在有一种需求,用户不想每次都去查阅图书列表了,当有新书时需要把书的信息告诉用户,这就是典型的观察者模式。首先,我们需要提供一个 AIDL 接口,每个用户都需要实现这个接口并向图书馆申请新书的提醒功能,用户也可以随时取消。在 AIDL 中只能使用 AIDL 接口,不能使用普通接口,我们需要创建一个 IOnNewBookArrivedListener.aidl 接口。
package com.mryuan.learndemo;
import com.mryuan.learndemo.Book;
interface IOnNewBookArrivedListener {
void onNewBookArrived(in Book newBook);
}
然后在 IBookManager 接口中添加两个新的方法,用于用户注册和取消注册。
package com.mryuan.learndemo;
import com.mryuan.learndemo.Book;
import com.mryuan.learndemo.IOnNewBookArrivedListener;
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
void registerListener(IOnNewBookArrivedListener listener);
void unregisterListener(IOnNewBookArrivedListener listener);
}
接着修改服务端 Service,主要是 IBookManager.Stub 的实现,因为我们在 IBookManager 新加了两个方法,所以在 IBookManager.Stub 中需要实现这两个方法。同时还开启了一个线程,每个 5s 就向书库中增加一本新书并通知注册过的用户。
public class BookManagerService extends Service {
private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<>();
private CopyOnWriteArrayList<IOnNewBookArrivedListener> mListenerList = new CopyOnWriteArrayList<>();
private AtomicBoolean mIsServiceDestroyed = new AtomicBoolean(false);
private Binder mBinder = new IBookManager.Stub() {
@Override
public List<Book> getBookList() throws RemoteException {
return mBookList;
}
@Override
public void addBook(Book book) throws RemoteException {
mBookList.add(book);
}
@Override
public void registerListener(IOnNewBookArrivedListener listener) throws RemoteException {
if (!mListenerList.contains(listener)) {
mListenerList.add(listener);
} else {
Log.d("TAG", "already exists.");
}
Log.d("TAG", "registerListener,size:" + mListenerList.size());
}
@Override
public void unregisterListener(IOnNewBookArrivedListener listener) throws RemoteException {
if (mListenerList.contains(listener)) {
mListenerList.remove(listener);
Log.d("TAG", "unregister listener succeed.");
} else {
Log.d("TAG", "not found,can not unregister.");
}
Log.d("TAG", "unregisterListener,current size:" + mListenerList.size());
}
};
@Override
public void onCreate() {
super.onCreate();
mBookList.add(new Book(1, "Android"));
mBookList.add(new Book(2, "IOS"));
new Thread(new ServiceWorker()).start();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
private class ServiceWorker implements Runnable {
@Override
public void run() {
while (!mIsServiceDestroyed.get()) {
try {
Thread.sleep(5000);
int bookId = mBookList.size() + 1;
Book newBook = new Book(bookId, "new book#" + bookId);
mBookList.add(newBook);
onNewBookArrived(newBook);
} catch (RemoteException | InterruptedException e) {
e.printStackTrace();
}
}
}
}
private void onNewBookArrived(Book book) throws RemoteException {
mBookList.add(book);
Log.d("TAG", "onNewBookArrived,notify listeners:" + mListenerList.size());
for (int i = 0; i < mListenerList.size(); i++) {
IOnNewBookArrivedListener listener = mListenerList.get(i);
Log.d("TAG", i + "onNewBookArrived,notify listener:" + listener);
listener.onNewBookArrived(book);
}
}
}
最后修改客户端,主要有两方面:首先客户端要注册 IOnNewBookArrivedListener 到远程服务器,这样当有新书时服务端才能通知当前客户端,同时要在 Activity 退出时解除注册;另一方面,当有新书时,服务端会回调客户端 IOnNewBookArrivedListener 对象的 onNewBookArrived 方法,但是这个方法是在客户端的 Binder 线程池中执行的,因此我们需要有个 Handler 切换到主线程用于 UI 操作。
public class MainActivity extends AppCompatActivity {
private static final int MESSAGE_NEW_BOOK_ARRIVED = 1;
private IBookManager mRemoteBookManager;
@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
switch (msg.what) {
case MESSAGE_NEW_BOOK_ARRIVED:
Log.d("TAG", "receive new book:" + msg.obj);
break;
default:
super.handleMessage(msg);
}
}
};
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
IBookManager bookManager = IBookManager.Stub.asInterface(service);
try {
mRemoteBookManager = bookManager;
List<Book> list = bookManager.getBookList();
Log.d("TAG", "query book list:" + list.toString());
Book newBook = new Book(3, "Android 开发艺术探索");
bookManager.addBook(newBook);
Log.d("TAG", "add book:" + newBook);
List<Book> newList = bookManager.getBookList();
Log.d("TAG", "query book list:" + newList.toString());
bookManager.registerListener(mOnNewBookArrivedListener);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
mRemoteBookManager = null;
Log.d("TAG", "binder died");
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent intent = new Intent(this, BookManagerService.class);
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onDestroy() {
if (mRemoteBookManager != null && mRemoteBookManager.asBinder().isBinderAlive()) {
try {
Log.d("TAG", "unregister listener:" + mOnNewBookArrivedListener);
mRemoteBookManager.unregisterListener(mOnNewBookArrivedListener);
} catch (RemoteException e) {
e.printStackTrace();
}
}
unbindService(mConnection);
super.onDestroy();
}
/**
* 这里要注意,一定要是 Stub 类
*/
private IOnNewBookArrivedListener mOnNewBookArrivedListener = new IOnNewBookArrivedListener.Stub() {
@Override
public void onNewBookArrived(Book newBook) throws RemoteException {
mHandler.obtainMessage(MESSAGE_NEW_BOOK_ARRIVED, newBook).sendToTarget();
}
};
}
private IOnNewBookArrivedListener mOnNewBookArrivedListener = new IOnNewBookArrivedListener() {
@Override
public void onNewBookArrived(Book newBook) throws RemoteException {
mHandler.obtainMessage(MESSAGE_NEW_BOOK_ARRIVED, newBook).sendToTarget();
}
@Override
public IBinder asBinder() {
return null;
}
};
}
运行程序,通过 log 可以看出,客户端的确收到了服务端每 5s 一次的新书推送。
实现解除注册
从上面的代码可以看出,当 BookManagerActivity 关闭时,我们会在 OnDestroy 中去解除已经注册到服务端的 listener,表示客户端不再想接收服务端的新书提醒了,所以我们可以随时取消这个提醒服务。但是上面的代码如果按 back 键退出 BookManagerActivity,会看到程序并没有像我们所预期的那样执行。在解注册过程中,服务端无法找到我们之前注册的 listener,解除注册失败。其实,这是必然的,这种解注册方式在日常开发过程中经常用到,但是放到多进程中却无法奏效,因为 Binder 会把客户端传递过来的对象重新转化并生成一个新的对象。虽然我们在注册和解注册过程中使用的是同一个客户端对象,但是通过 Binder 传递到服务端后,却会产生两个全新的对象。因为对象是不能跨进程传输的,对象的跨进程传输本质都是反序列化的过程,这就是 AIDL 中的自定义对象都必须要实现 Parcelable 接口的原因。
其实,系统专门提供了一个用于删除跨进程 listener 的接口 RemoteCallbackList,RemoteCallbackList 是一个泛型,支持管理任意的 AIDL 接口,这点从它的声明就可以看出,因为所有的 AIDL 接口都继承字 IInterface 接口,这点在上一篇博客有提过。
它的工作原理很简单,在它的内部有一个 Map 结构专门用来保存所有的 AIDL 回调,这个 Map 的 key 是 IBinder 类型,value 是 Callback 类型,其中 Callback 中封装了真正的远程 listener。当客户端注册 listener 的时候,它会把这个 listener 信息存入 mCallback 中。其中 key 和 value 的获取方式如下。
public class RemoteCallbackList<E extends IInterface> {
...
@UnsupportedAppUsage
/*package*/ ArrayMap<IBinder, Callback> mCallbacks
= new ArrayMap<IBinder, Callback>();
...
private final class Callback implements IBinder.DeathRecipient {
...
Callback(E callback, Object cookie) {
mCallback = callback;
mCookie = cookie;
}
...
}
...
}
IBinder key = listener.asBinder();
Callback value = new Callback(listener,cookie);
到这里可以看出,虽然说多次跨进程传输客户端的同一个对象会在服务端生成不同的对象,但是这些新生成的对象有一个共同点,那就是它们底层的 Binder 对象是同一个,利用这个特性,就可以实现解注册。当客户端解注册的时候,我们只要遍历服务端所有的 listener,找出那个和解注册 listener 具有相同 Binder 对象的服务端 listener 并把它删掉即可。同时 RemoteCallbackList 在客户端进程终止后,它能够自动移除客户端所注册的 listener。并且,RemoteCallbackList 内部还自动实现了线程同步的功能,所以我们使用它来注册和解注册时,不需要做额外的线程同步工作。
现在我们来实现解注册,首先修改 BookManagerService,创建一个 RemoteCallbackList 对象来替代 CopyOnWriteArrayList。修改 registerListener 和 unregisterListener 两个接口的实现和 onNewBookArrived 方法。
使用 RemoteCallbackList,有一点需要注意,我们无法像操作 List 一样去操作它,它并不是一个 List。遍历 RemoteCallbackList 跟 List 有所不同,使用的是 beginBroadcast 方法,其中 beginBroadcast 还需要 finishBroadcast 配合使用,代码如下。
private RemoteCallbackList<IOnNewBookArrivedListener> mListenerList = new RemoteCallbackList<>();
@Override
public void registerListener(IOnNewBookArrivedListener listener) throws RemoteException {
mListenerList.register(listener);
final int N = mListenerList.beginBroadcast();
mListenerList.finishBroadcast();
Log.d("TAG", "registerListener, current size:" + N);
}
@Override
public void unregisterListener(IOnNewBookArrivedListener listener) throws RemoteException {
boolean success = mListenerList.unregister(listener);
if (success) {
Log.d("TAG", "unregister success.");
} else {
Log.d("TAG", "not found, can not unregister.");
}
final int N = mListenerList.beginBroadcast();
mListenerList.finishBroadcast();
Log.d("TAG", "unregisterListener, current size:" + N);
}
private void onNewBookArrived(Book book) throws RemoteException {
mBookList.add(book);
final int N = mListenerList.beginBroadcast();
for (int i = 0; i < N; i++) {
IOnNewBookArrivedListener listener = mListenerList.getBroadcastItem(i);
if (listener != null) {
try {
listener.onNewBookArrived(book);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
mListenerList.finishBroadcast();
}
运行结果,在注册和解注册时我们还分别打印出了所有 listener 的数量。
客户端和服务端相互调用耗时方法
客户端调用远程服务的方法,被调用的方法运行在服务端 Binder 线程池中,同时客户端线程会被挂起,此时如果服务端的方法执行比较耗时,就会导致客户端线程阻塞,而如果客户端线程是 UI 线程的话,就会导致 ANR。因此,如果我们明确知道某个远程方法是耗时的,那么就要避免在客户端的 UI 线程中去访问远程方法。
客户端的 onServiceConnected 和 onServiceDisconnected 方法都运行在 UI 线程中,所以也不可以在它们里面直接调用服务端的耗时方法。另外,由于服务端的方法本身就运行在服务端的 Binder 线程池中,所以服务端方法本身就可以执行大量耗时操作,此时也不要在服务端方法中开线程去进行异步任务,除非你明确知道自己在干什么,否则不建议这么做。
接下来举一个例子,修改服务端的 getBookList 方法,在客户端放一个按钮,单击时调用服务端的方法,连续单击按钮,即连续调用服务端的 getBookList 方法,客户端就会 ANR。
@Override
public List<Book> getBookList() throws RemoteException {
SystemClock.sleep(5000);
return mBookList;
}
解决方法很简单,只需把调用放到非 UI 线程即可。在本例中需要注意,应该在 onServiceConnected 方法执行完毕后再点击按钮,因为 onServiceConnected 方法中也调用了 getBookList 方法,如果此时点击按钮,也会 ANR。
findViewById(R.id.click).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
if (mRemoteBookManager != null) {
try {
mRemoteBookManager.getBookList();
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
}).start();
}
});
同理,当远程服务端需要调用客户端 listener 中的方法时,被调用的方法也运行在 Binder 线程池中,只不过是客户端的线程池。所以,同样不可以在服务端中调用客户端的耗时方法。比如针对 BookManagerService 的onNewBookArrived 方法,它内部调用了客户端的 IOnNewBookArrivedListener 中的 onNewBookArrived 方法,如果客户端的这个 onNewBookArrived 方法比较耗时的话,那么请确保 BookManagerService 中的 onNewBookArrived 运行在非 UI 线程中,否则将导致服务端无法响应。
private void onNewBookArrived(Book book) throws RemoteException {
mBookList.add(book);
final int N = mListenerList.beginBroadcast();
for (int i = 0; i < N; i++) {
IOnNewBookArrivedListener listener = mListenerList.getBroadcastItem(i);
if (listener != null) {
try {
listener.onNewBookArrived(book);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
mListenerList.finishBroadcast();
}
另外,由于客户端 IOnNewBookArrivedListener 中的 onNewBookArrived 方法运行在客户端的 Binder 线程池中,所以不能在它里面去访问 UI 相关的内容,如果要访问 UI,就需要使用 Handler 切换到 UI 线程,这点在前面的代码中可以看出。
为了程序的健壮性,我们还需要做一件事。Binder 是可能意外死亡的,这往往是由于服务端进程意外停止了,这是我们需要重新连接服务。有两种办法,第一种是给 Binder 设置 DeathRecipient 监听,当 Binder 死亡时,我们会收到 binderDied 方法的回调,在 binderDied 方法中我们可以重连远程服务,具体方法在上一篇的 Binder 中介绍过。另一种方法是在 onServiceConnected 中重连远程服务。这两种方法可以随便选择一种来使用,区别在于:onServiceConnected 在客户端的 UI 线程中被回调,而 binderDied 在客户端的 Binder 线程池中被回调。也就是说,在 binderDied 方法中我们不能访问 UI,这就是它们的区别。
使用 DDMS 查看线程,找到 SDK 安装位置,在 tools 文件夹中双击 monitor.bat,会弹出一个命令窗口,等待几秒,DDMS 就会打开。需要注意打开顺序,先打开 DDMS,再打开 APP,最后打开 AndroidStudio,不然会出现端口被占用的情况。
权限验证功能
默认情况下,我们的远程服务任何人都可以连接,但这不是我们愿意看到的,所以我们必须给服务加入权限验证功能,权限验证失败则无法调用服务中的方法。我们可以在 onBind 中进行验证,验证不通过就直接返回 null,这样验证失败的客户端直接无法绑定服务。验证的方式有多种,下面是两种常用的方法。
第一种方法,使用 permission 验证,使用这种方式,我们要先在 AndroidManifest 中声明所需的权限。定义了权限后(自定义权限自行查询),就可以在 BookManagerService 的 onBind 方法中做权限验证了。一个应用来绑定服务时,会验证这个应用的权限,如果它没有使用这个权限,onBind 方法就会直接返回 null,这个应用就无法绑定到服务。如果是内部的应用想绑定到服务中,只需要在它的 AndroidManifest 文件中申请权限即可。
@Nullable
@Override
public IBinder onBind(Intent intent) {
int check = checkCallingOrSelfPermission("申请的权限");
if (check == PackageManager.PERMISSION_DENIED) {
return null;
}
return mBinder;
}
第二种方法,可以在服务端的 onTransact 方法中进行权限验证,如果验证失败就直接返回 false,这样服务端就不会终止执行 AIDL 中的方法从而达到保护服务端的效果。至于具体的验证方式有很多,可以采用 permission 验证,具体实现方式和第一种方法一样。还可以采用 Uid 和 Pid 来做验证,通过 getCallingUid 和 getCallingPid 可以拿到客户端所属应用的 Uid 和 Pid,通过这两个参数我们可以做一些验证工作,比如验证包名。在下面的代码中,即验证了 permission,又验证了包名。一个应用如果想远程调用服务中的方法,首先要使用我们的自定义权限 “申请的权限”,其次包名必须以 “com.mryuan” 开始,否则调用服务端的方法会失败。
private Binder mBinder = new IBookManager.Stub() {
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
int check = checkCallingOrSelfPermission("申请的权限");
if (check == PackageManager.PERMISSION_DENIED) {
return false;
}
String packageName = null;
String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
if (packages != null && packages.length > 0) {
packageName = packages[0];
}
if (!packageName.startsWith("com.mryuan")) {
return false;
}
return super.onTransact(code, data, reply, flags);
}
...
}