[TOC]

概述

使用Messenger来进行进程间通信的方法,可以发现,Messenger是以串行的方式处理客户端发来的消息,如果大量的消息同时发送到服务端,服务端仍然只能一个个处理,如果有大量的并发请求,那么用Messenger就不太合适了。

同时,Messenger的作用主要是为了传递消息,很多时候我们可能需要跨进程调用服务端的方法,这种情形用Messenger就无法做到了,但是我们可以使用AIDL来实现跨进程的方法调用。AIDL也是Messenger的底层实现,因此Messenger本质上也是AIDL,只不过系统为我们做了封装从而方便上层的调用而已

所以,下面我们重点学习一下AIDL的基础知识。

什么是AIDL

AIDL(Android Interface Definition Language) Android接口定义语言 利用它定义客户端与服务均认可的编程接口,以便二者使用进程间通信 (IPC) 进行相互通信。实际上起作用的并不是AIDL文件,而是根据AIDL生成的实例代码,AIDL是安卓替我们设计好的一个模板,根据模板生成Interface的代码。

ADIL的存在就是为了实现进程间的通信,通过AIDL我们可以在不同进程中进行数据通信与逻辑交互。

在Binder的基础上我们可以更加容易地理解AIDL。这里先介绍使用AIDL来进行进程间通信的流程,分为服务端和客户端两个方面。

Server端(服务端)

服务端首先要创建一个Service用来监听客户端的连接请求,然后创建一个AIDL文件,将暴露给客户端的接口在这个AIDL文件中声明,最后在Service中实现这个AIDL接口即可。

Client端(客户端)

客户端所要做事情就稍微简单一些,首先需要绑定服务端的Service,绑定成功后,将服务端返回的Binder对象转成AIDL接口所属的类型,接着就可以调用AIDL中的方法了。

上面,只是简单的介绍了AIDL的相关逻辑。真正实现起来,还是比较复杂的。

下面,我们就一步步来实现一个AIDL的跨进程通信的Demo

1.AIDL接口的创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// IRemoteServiceInterface.aidl
package com.frewen.android.demo;
// Declare any non-default types here with import statements
import com.frewen.android.demo.RemoteTicket;
import com.frewen.android.demo.IOnNewTicketArrivedListener;
/**
* 在AIDL文件中,并不是所有的数据类型都是可以使用的,那么到底AIDL文件支持哪些数据类型呢?如下所示。
* 基本数据类型(int、long、char、boolean、double等);
* String和CharSequence;
* List:只支持ArrayList,里面每个元素都必须能够被AIDL支持;
* Map:只支持HashMap,里面的每个元素都必须被AIDL支持,包括key和value;
* Parcelable:所有实现了Parcelable接口的对象;
* AIDL:所有的AIDL接口本身也可以在AIDL文件中使用。
* 以上6种数据类型就是AIDL所支持的所有类型,
* 其中自定义的Parcelable对象和AIDL对象必须要显式import进来,
* 不管它们是否和当前的AIDL文件位于同一个包
**/
interface IRemoteServiceInterface {
/**
* 获取远程服务的pid
**/
int getPid();

List<RemoteTicket> getTicketList();

void addTicket(in RemoteTicket ticket);

void registerListener(IOnNewTicketArrivedListener listener);

void unregisterListener(IOnNewTicketArrivedListener listener);
/**
* 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);
}

需要注意:如果AIDL文件中用到了自定义的Parcelable对象,那么必须新建一个和它同名的AIDL文件,并在其中声明它为Parcelable类型。在上面的IRemoteServiceInterface.aidl中,我们用到了RemoteTicket这个类,所以,我们必须要创建RemoteTicket.aidl,然后在里面添加如下内容:

1
2
3
4
5
6
// RemoteTicket.aidl
// 这个包名就是我们所要放置的RemoteTicket包名
package com.frewen.android.demo;
//如果AIDL文件中用到了自定义的Parcelable对象,那么必须新建一个和它同名的AIDL文件,并在其中声明它为Parcelable类型
parcelable RemoteTicket;

AIDL中每个实现了Parcelable接口的类都需要按照上面那种方式去创建相应的AIDL文件并声明那个类为parcelable。除此之外,AIDL中除了基本数据类型,其他类型的参数必须标上方向:in、out或者inout,in表示输入型参数,out表示输出型参数,inout表示输入输出型参数,至于它们具体的区别,这个就不说了。我们要根据实际需要去指定参数类型,不能一概使用out或者inout,因为这在底层实现是有开销的。最后,AIDL接口中只支持方法,不支持声明静态常量,这一点区别于传统的接口。

为了方便AIDL的开发,建议把所有和AIDL相关的类和文件全部放入同一个包中,这样做的好处是,当客户端是另外一个应用时,我们可以直接把整个包复制到客户端工程中。

注意。通过测试发现。AIDL相关的文件以及类文件,必须要要放在同一个包里面。不然编译报错。这是一个坑!!!

2、 我们创建一个服务端处理逻辑对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/**
* @filename: RemoteTicketServer
* @introduction: 服务端关于客户端调用接口的处理逻辑
* <p>
* * 实例化Binder的对象。通过AIDL文件生成对应的Binder对象。
* * 然后通过操作AIDL中的方法,来给客户端返回服务端的数据
* @author: Frewen.Wong
* @time: 2019/9/4 0004 下午8:47
* Copyright ©2019 Frewen.Wong. All Rights Reserved.
*/
public class RemoteTicketServer extends IRemoteServiceInterface.Stub {
private static final String TAG = "T:RemoteTicketServer";
private final Context mContext;
private CopyOnWriteArrayList<RemoteTicket> mTicketList = new CopyOnWriteArrayList<>();
/**
* 实例化CallbackListener
* 使用RemoteCallbackList,有一点需要注意,我们无法像操作List一样去操作它,尽管它的名字中也带个List,但是它并不是一个List。
*/
private RemoteCallbackList<IOnNewTicketArrivedListener> mListenerList = new RemoteCallbackList<>();

public RemoteTicketServer(Context mContext) {
this.mContext = mContext;
}

@Override
public int getPid() throws RemoteException {
// 模拟一个跨进程的耗时任务
SystemClock.sleep(6000);
return android.os.Process.myPid();
}

/**
* 获取演唱会列表
*
* @return
* @throws RemoteException
*/
@Override
public List<RemoteTicket> getTicketList() throws RemoteException {
Log.d(TAG, "FMsg:getTicketList() begin");
// 获取门票列表,模拟一个耗时任务的实现
SystemClock.sleep(30 * 1000);
Log.d(TAG, "FMsg:getTicketList() end currentThread = " + Thread.currentThread().getName());
return mTicketList;
}

@Override
public void addTicket(RemoteTicket ticket) throws RemoteException {
Log.d(TAG, "FMsg:addTicket() called with: ticket = [" + ticket + "]");
onNewTicketArrived(ticket);
}

/**
* 定义当有新的门票添加的回调方法
*
* @param ticket
* @throws RemoteException
*/
private void onNewTicketArrived(RemoteTicket ticket) throws RemoteException {
mTicketList.add(ticket);
/**
* 遍历RemoteCallbackList,必须要按照下面的方式进行,
* 其中beginBroadcast和beginBroadcast必须要配对使用,
* 哪怕我们仅仅是想要获取RemoteCallbackList中的元素个数,这是必须要注意的地方。
*/
final int N = mListenerList.beginBroadcast();
for (int i = 0; i < N; i++) {
IOnNewTicketArrivedListener listener = mListenerList.getBroadcastItem(i);
if (listener != null) {
try {
/**
* 远程服务端需要调用客户端的listener中的方法时,被调用的方法也运行在Binder线程池中,
* 只不过是客户端的线程池。所以,我们同样不可以在服务端中调用客户端的耗时方法。
* 比如针对RemoteTicketServer的onNewTicketArrived方法,
* 如下所示。在它内部调用了客户端的IOnNewBookArrivedListener中的onNewBookArrived方法,
* 如果客户端的这个onNewTicketArrived方法比较耗时的话,
* 那么请确保BRemoteTicketServer中的onNewTicketArrived运行在非UI线程中,否则将导致服务端无法响应。
*/
// 回调给客户端当有新的门票被添加的逻辑
listener.onNewTicketArrived(ticket);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
mListenerList.finishBroadcast();
}

@Override
public void registerListener(IOnNewTicketArrivedListener listener) throws RemoteException {
Log.d(TAG, "FMsg:registerListener() called with: listener = [" + listener.getClass() + "]");
Log.d(TAG, "FMsg:registerListener() called with: mListenerList = [" + mListenerList + "]");
// 注册当前的listener
mListenerList.register(listener);
final int N = mListenerList.beginBroadcast();
mListenerList.finishBroadcast();
Log.d(TAG, "registerListener, current size:" + N);
}

@Override
public void unregisterListener(IOnNewTicketArrivedListener 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);
}

@Override
public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {

}

public void addTicketList(CopyOnWriteArrayList<RemoteTicket> ticketList) {
Log.d(TAG, "FMsg:addTicketList() called with: ticketList = [" + ticketList + "]");
mTicketList.addAll(ticketList);
}

/**
* 第二种方法,我们可以在服务端的onTransact方法中进行权限验证,如果验证失败就直接返回false,
* 这样服务端就不会终止执行AIDL中的方法从而达到保护服务端的效果。至于具体的验证方式有很多,
* 可以采用permission验证,具体实现方式和第一种方法一样。还可以采用Uid和Pid来做验证,
* 通过getCallingUid和getCallingPid可以拿到客户端所属应用的Uid和Pid,
* 通过这两个参数我们可以做一些验证工作,比如验证包名。在下面的代码中,既验证了permission,
* 又验证了包名。一个应用如果想远程调用服务中的方法,
* 首先要使用我们的自定义权限“com.frewen.android.demo.permission.ACCESS_TICKET_SERVICE”,
* 其次包名必须以“com.frewen.android.demo.permission.ACCESS_TICKET_SERVICE”开始,否则调用服务端的方法会失败。
*
* 注意:这个方法如果不是跨进程继续绑定的话。这个方法是不会调用的。
* 换句话说:这种方法只能作为跨进程绑定远程服务的权限判断接口
* @param code
* @param data
* @param reply
* @param flags
* @return
* @throws RemoteException
*/
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
throws RemoteException {
Log.d(TAG, "FMsg:onTransact() called with: code = [" + code + "], data = [" + data + "], reply = [" + reply + "], flags = [" + flags + "]");
int check = mContext.checkCallingOrSelfPermission(
"com.frewen.android.demo.permission.ACCESS_TICKET_SERVICE");
if (check == PackageManager.PERMISSION_DENIED) {
return false;
}
String packageName = null;
// 这个是packageManager里面的方法。通过Uid来拿到进程的包名
String[] packages = mContext.getPackageManager().getPackagesForUid(getCallingUid());
if (packages != null && packages.length > 0) {
packageName = packages[0];
}
Log.d(TAG, "FMsg:onTransact packageName = [" + packageName + "]");
if (!packageName.startsWith("com.frewen.android.demo")) {
return false;
}
return super.onTransact(code, data, reply, flags);
}
}

3、创建服务端Service对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/**
* @filename: RemoteTicketServer
* @introduction: 服务端关于客户端调用接口的处理逻辑
* <p>
* * 实例化Binder的对象。通过AIDL文件生成对应的Binder对象。
* * 然后通过操作AIDL中的方法,来给客户端返回服务端的数据
* @author: Frewen.Wong
* @time: 2019/9/4 0004 下午8:47
* Copyright ©2019 Frewen.Wong. All Rights Reserved.
*/
public class RemoteTicketServer extends IRemoteServiceInterface.Stub {
private static final String TAG = "T:RemoteTicketServer";
private final Context mContext;
private CopyOnWriteArrayList<RemoteTicket> mTicketList = new CopyOnWriteArrayList<>();
/**
* 实例化CallbackListener
* 使用RemoteCallbackList,有一点需要注意,我们无法像操作List一样去操作它,尽管它的名字中也带个List,但是它并不是一个List。
*/
private RemoteCallbackList<IOnNewTicketArrivedListener> mListenerList = new RemoteCallbackList<>();

public RemoteTicketServer(Context mContext) {
this.mContext = mContext;
}

@Override
public int getPid() throws RemoteException {
// 模拟一个跨进程的耗时任务
SystemClock.sleep(6000);
return android.os.Process.myPid();
}

/**
* 获取演唱会列表
*
* @return
* @throws RemoteException
*/
@Override
public List<RemoteTicket> getTicketList() throws RemoteException {
Log.d(TAG, "FMsg:getTicketList() begin");
// 获取门票列表,模拟一个耗时任务的实现
SystemClock.sleep(30 * 1000);
Log.d(TAG, "FMsg:getTicketList() end currentThread = " + Thread.currentThread().getName());
return mTicketList;
}

@Override
public void addTicket(RemoteTicket ticket) throws RemoteException {
Log.d(TAG, "FMsg:addTicket() called with: ticket = [" + ticket + "]");
onNewTicketArrived(ticket);
}

/**
* 定义当有新的门票添加的回调方法
*
* @param ticket
* @throws RemoteException
*/
private void onNewTicketArrived(RemoteTicket ticket) throws RemoteException {
mTicketList.add(ticket);
/**
* 遍历RemoteCallbackList,必须要按照下面的方式进行,
* 其中beginBroadcast和beginBroadcast必须要配对使用,
* 哪怕我们仅仅是想要获取RemoteCallbackList中的元素个数,这是必须要注意的地方。
*/
final int N = mListenerList.beginBroadcast();
for (int i = 0; i < N; i++) {
IOnNewTicketArrivedListener listener = mListenerList.getBroadcastItem(i);
if (listener != null) {
try {
/**
* 远程服务端需要调用客户端的listener中的方法时,被调用的方法也运行在Binder线程池中,
* 只不过是客户端的线程池。所以,我们同样不可以在服务端中调用客户端的耗时方法。
* 比如针对RemoteTicketServer的onNewTicketArrived方法,
* 如下所示。在它内部调用了客户端的IOnNewBookArrivedListener中的onNewBookArrived方法,
* 如果客户端的这个onNewTicketArrived方法比较耗时的话,
* 那么请确保BRemoteTicketServer中的onNewTicketArrived运行在非UI线程中,否则将导致服务端无法响应。
*/
// 回调给客户端当有新的门票被添加的逻辑
listener.onNewTicketArrived(ticket);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
mListenerList.finishBroadcast();
}

@Override
public void registerListener(IOnNewTicketArrivedListener listener) throws RemoteException {
Log.d(TAG, "FMsg:registerListener() called with: listener = [" + listener.getClass() + "]");
Log.d(TAG, "FMsg:registerListener() called with: mListenerList = [" + mListenerList + "]");
// 注册当前的listener
mListenerList.register(listener);
final int N = mListenerList.beginBroadcast();
mListenerList.finishBroadcast();
Log.d(TAG, "registerListener, current size:" + N);
}

@Override
public void unregisterListener(IOnNewTicketArrivedListener 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);
}

@Override
public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {

}

public void addTicketList(CopyOnWriteArrayList<RemoteTicket> ticketList) {
Log.d(TAG, "FMsg:addTicketList() called with: ticketList = [" + ticketList + "]");
mTicketList.addAll(ticketList);
}

/**
* 第二种方法,我们可以在服务端的onTransact方法中进行权限验证,如果验证失败就直接返回false,
* 这样服务端就不会终止执行AIDL中的方法从而达到保护服务端的效果。至于具体的验证方式有很多,
* 可以采用permission验证,具体实现方式和第一种方法一样。还可以采用Uid和Pid来做验证,
* 通过getCallingUid和getCallingPid可以拿到客户端所属应用的Uid和Pid,
* 通过这两个参数我们可以做一些验证工作,比如验证包名。在下面的代码中,既验证了permission,
* 又验证了包名。一个应用如果想远程调用服务中的方法,
* 首先要使用我们的自定义权限“com.frewen.android.demo.permission.ACCESS_TICKET_SERVICE”,
* 其次包名必须以“com.frewen.android.demo.permission.ACCESS_TICKET_SERVICE”开始,否则调用服务端的方法会失败。
*
* 注意:这个方法如果不是跨进程继续绑定的话。这个方法是不会调用的。
* 换句话说:这种方法只能作为跨进程绑定远程服务的权限判断接口
* @param code
* @param data
* @param reply
* @param flags
* @return
* @throws RemoteException
*/
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
throws RemoteException {
Log.d(TAG, "FMsg:onTransact() called with: code = [" + code + "], data = [" + data + "], reply = [" + reply + "], flags = [" + flags + "]");
int check = mContext.checkCallingOrSelfPermission(
"com.frewen.android.demo.permission.ACCESS_TICKET_SERVICE");
if (check == PackageManager.PERMISSION_DENIED) {
return false;
}
String packageName = null;
// 这个是packageManager里面的方法。通过Uid来拿到进程的包名
String[] packages = mContext.getPackageManager().getPackagesForUid(getCallingUid());
if (packages != null && packages.length > 0) {
packageName = packages[0];
}
Log.d(TAG, "FMsg:onTransact packageName = [" + packageName + "]");
if (!packageName.startsWith("com.frewen.android.demo")) {
return false;
}
return super.onTransact(code, data, reply, flags);
}
}

我们在使用registerListener的时候报了一个异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java.lang.NullPointerException: Attempt to invoke interface method 'void android.os.IBinder.linkToDeath(android.os.IBinder$DeathRecipient, int)' on a null object reference
at android.os.RemoteCallbackList.register(RemoteCallbackList.java:114)
at android.os.RemoteCallbackList.register(RemoteCallbackList.java:78)
at com.frewen.android.demo.samples.ipc.remote.aidl.RemoteTicketServer.registerListener(RemoteTicketServer.java:84)
at com.frewen.android.demo.samples.ipc.client.AIDLDemoActivity$3.onServiceConnected(AIDLDemoActivity.java:87)
at android.app.LoadedApk$ServiceDispatcher.doConnected(LoadedApk.java:1453)
at android.app.LoadedApk$ServiceDispatcher$RunConnection.run(LoadedApk.java:1481)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6121)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:889)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:779)

后来定位到原因之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
private IOnNewTicketArrivedListener mOnNewBookArrivedListener = new IOnNewTicketArrivedListener.Stub() {
@Override
public void onNewTicketArrived(RemoteTicket newTicket) throws RemoteException {
Log.d(TAG, "FMsg:onNewTicketArrived() called with: thread = [" + Thread.currentThread().getName() + "]");
mHandler.obtainMessage(MESSAGE_NEW_TICKET_ARRIVED, newTicket)
.sendToTarget();
}

@Override
public IBinder asBinder() {
return null;
}
};

如果我们再遇到另外一个问题:

1
2
3
4
5
6
7
8
java.lang.IllegalStateException: beginBroadcast() called while already in a broadcast
at android.os.RemoteCallbackList.beginBroadcast(RemoteCallbackList.java:229)
at com.frewen.android.demo.samples.ipc.remote.aidl.RemoteTicketServer.onNewTicketArrived(RemoteTicketServer.java:65)
at com.frewen.android.demo.samples.ipc.remote.aidl.RemoteTicketServer.addTicket(RemoteTicketServer.java:55)
at com.frewen.android.demo.samples.ipc.remote.aidl.RemoteService.addNewTicket(RemoteService.java:99)
at com.frewen.android.demo.samples.ipc.remote.aidl.RemoteService.access$200(RemoteService.java:24)
at com.frewen.android.demo.samples.ipc.remote.aidl.RemoteService$ServiceWorker.run(RemoteService.java:85)
at java.lang.Thread.run(Thread.java:761)

这个问题的主要由原因是我们没有进行广播的结束

1
mListenerList.finishBroadcast();

AIDL的权限验证

如何在AIDL中使用权限验证功能。默认情况下,我们的远程服务任何人都可以连接,但这应该不是我们愿意看到的,所以我们必须给服务加入权限验证功能,权限验证失败则无法调用服务中的方法。在AIDL中进行权限验证,这里介绍两种常用的方法。

第一种方法,我们可以在onBind中进行验证,验证不通过就直接返回null,这样验证失败的客户端直接无法绑定服务,至于验证方式可以有多种,比如使用permission验证。使用这种验证方式,我们要先在AndroidMenifest中声明所需的权限,比如:

1
2
3
4
5
<!--  使用permission验证。使用这种验证方式,我们要先在AndroidMenifest中声明所需的权限  -->
<!-- AIDL的学习:在这个地方声明绑定购票服务的权限 -->
<permission
android:name="com.frewen.android.demo.permission.ACCESS_TICKET_SERVICE"
android:protectionLevel="normal" />

AIDL的基础知识汇总。

1、AIDL会阻塞住客户端主线程吗?

我们知道:客户端调用远程服务的方法,被调用的方法运行在服务端的Binder线程池中, 同时客户端线程会被挂起.

这个时候如果服务端方法执行比较耗时,就会导致客户端线程长时间地阻塞在这里,而如果这个客户端线程是UI线程的话,就会导致客户端ANR,这当然不是我们想要看到的。因此,如果我们明确知道某个远程方法是耗时的,那么就要避免在客户端的UI线程中去访问远程方法。

另外由于客户端的onServiceConnected和onService Disconnected方法都运行在UI线程中,所以也不可以在它们里面直接调用服务端的耗时方法