责任链模式

说到责任链模式一定会想到各种拦截器,在很多框架中都有拦截器的使用,如常见的okhttp中的请求处理拦截器,路由框架中的路由拦截器,作为框架的使用方你可能已经对拦截器司空见惯,但是不可否认的是一个好的设计经常会有让人有眼前一亮的感觉。本文尝试总结一下在安卓框架和源码中,责任链模式的使用场景和设计方法。

0x01 什么是责任链模式

从设计模式的角度,拦截器的设计常常称之为责任链模式(Chain of Responsibility)。责任链模式的定义:为了避免请求发送者与多个请求处理者耦合在一起,于是将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。

责任链模式的优点:

  1. 降低耦合,不需要知道整条处理链路的结构,发送者和接受者也无需知道对方的确切信息。
  2. 灵活,扩展性强,可根据需要灵活增加处理流程,可动态调整处理顺序
  3. 功能内聚,单一职责,每个类只需要关注自己处理的工作,不该处理的传递给下一个对象处理

责任链模式的缺点:

  1. 复杂度较高,性能可能受影响
  2. 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。

    0x02 okhttp中的阑尾式拦截器

    在RealCall.java中内置了5个拦截器。创建一个包含所有拦截器的拦截器链表。在RealInterceptorChain的proceed方法中根据index获取当前拦截器,在拦截器的intercept方法内部递归调用RealInterceptorChain的proceed方法并将index+1,更新到下一个拦截器。这里需要注意的是,最后一个拦截器的实现,在最后一个拦截器CallServerInterceptor中,并不会再调用Chain的proceed方法,而是直接根据网络结果返回response。这里可以看做整个拦截器的尽头,在胡同走到尽头后再根据原路逐级返回response。所以像是走进胡同的拦截器调用算不算半截阑尾?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// 拦截器起点和终点
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(retryAndFollowUpInterceptor);
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}
interceptors.add(new CallServerInterceptor(forWebSocket));

Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());

return chain.proceed(originalRequest);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
/// 拦截器迭代
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {

// Call the next interceptor in the chain.
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
writeTimeout);
Interceptor interceptor = interceptors.get(index);
Response response = interceptor.intercept(next);

return response;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// 单个拦截器递归回调到拦截器链
public Response intercept(Chain chain) throws IOException {
Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);
} finally {
// If we're crashing on I/O or otherwise, don't leak the cache body.
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}

return response;
}

0x03 wmrouter中的洋葱式拦截器

和okhttp的拦截器相比,wmrouter中的拦截器差异在于他并没有response返回值,拦截器之间通过callback的方式返回结果或中断拦截流程。个人理解主要有以下几点考虑。

  • 并不需要像okhttp那样针对队尾的拦截器做特殊实现,所有拦截器的实现只要考虑是next还是complete即可。这样拦截器组合和嵌套更灵活。
  • 支持异步拦截回调

wmrouter在查找路由的过程中,使用了多层UriHandler和Interceptor嵌套的拦截器,在UriHandler的handle方法中,通过shouldHandle是否被当前的Handler拦截,再通过是否包含拦截器来确定是handler拦截还是Interceptor拦截,如果是Interceptor拦截则进入下一层。
所以相比较okhttp的拦截器,wmrouter的拦截器模式分层更明显,更像洋葱。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void handle(@NonNull final UriRequest request, @NonNull final UriCallback callback) {
if (shouldHandle(request)) {
if (mInterceptor != null && !request.isSkipInterceptors()) {
mInterceptor.intercept(request, new UriCallback() {
@Override
public void onNext() {
handleInternal(request, callback);
}

@Override
public void onComplete(int result) {
callback.onComplete(result);
}
});
} else {
handleInternal(request, callback);
}
} else {
callback.onNext();
}
}

在ChainedInterceptor中根据递归调用next方法更新拦截器迭代,通过Callback回溯到上一层,这里的上一层可能是Interceptor也可能是UriHandler。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void next(@NonNull final Iterator<UriInterceptor> iterator, @NonNull final UriRequest request,
@NonNull final UriCallback callback) {
if (iterator.hasNext()) {
UriInterceptor t = iterator.next();
t.intercept(request, new UriCallback() {
@Override
public void onNext() {
next(iterator, request, callback);
}

@Override
public void onComplete(int resultCode) {
callback.onComplete(resultCode);
}
});
} else {
callback.onNext();
}
}

可以参照下图理解

可以参照wmrouter官方文档的流程图理解ChainedHandler和ChainedInterceptor的多层嵌套关系。

0x04 Touch事件分发的贪食蛇拦截

你一定看过类似这样的事件分发流程图(出自https://www.gcssloop.com/customview/dispatch-touchevent-theory):

在touch分发的流程中三个关键的方法,dispatchTouchEvent(),interceptTouchEvent(),touchEvent()分别代表事件分发,事件拦截,事件消费。你也一定听过所谓的U型事件传递路径,从Activity开始分发,到ViewGroup,再到View。根据方法返回值是true还是false来决定上述的三个方法是否分发、拦截或消费touch事件。参考如下伪代码理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public boolean dispatchTouchEvent(MotionEvent ev) {
// 默认状态为没有消费过
boolean result = false;

if (!onInterceptTouchEvent(ev)) {
// 如果没有拦截就直接分发给子View
result = child.dispatchTouchEvent(ev);
}

if (!result) {
// 如果事件没有子View消费或者是被自己拦截了,询问自身onTouchEvent
result = onTouchEvent(ev);
}

return result;
}

如果从责任链的角度来看,你是否想过在整个事件分发的流程中,整套机制的最终目标是什么?

  • 其实事件分发的目标,是为了找到负责消费整个touch流程的view。这里的touch流程包含了DOWN、MOVE和UP事件。而CANCEL事件可以理解成一个error信号,只有当子视图消费的前驱事件,但是又被拦截了当前的事件才会收到CANCEL类型的事件。
  • 一次触摸流程中产生事件应被同一 View 消费,全部接收或者全部拒绝。
  • 所以才有了所谓的只有消费了DOWN事件才会接受MOVE和UP事件这样的分发规则。可以想象成一条由DOWN、MOVE和UP事件组成的贪食蛇。蛇身和蛇尾的运动是根据蛇头来的。事件分发机制先找到DOWN事件的消费者,再根据DOWN事件的touchTarget,将MOVE和UP事件分发下去。
  • 所谓的U型传递,从ViewGroup到View,再从View到ViewGroup的事件传递,只可能是DOWN类型的事件

当parent和child同时设置了click事件监听,为什么是child优先响应?
因为click事件是在onTouchEvent中响应的,而onTouchEvent的消费顺序是先child后parent,当view设置了touchListener或者是clickListener,事件就会被view拦截

0x05 总结

  • 责任链在设计实现上,往往通过链表或者递归调用的方式,将请求或事件依次从头结点向下传递,并回溯
  • 在责任链的任意节点上,都可以根据情况决定是否终止在责任链上的事件传递,okhttp通过抛异常的方式终止,wmroutor通过callback依次退栈

参考

http://c.biancheng.net/view/1383.html
https://www.debug8.com/javascript/t_66952.html
https://segmentfault.com/a/1190000012227736
https://www.gcssloop.com/customview/dispatch-touchevent-theory
https://blog.csdn.net/lfdfhl/article/details/50707724