Glide源码阅读之状态模式[SingleRequest<R>.Status]
前言
前面写完策略模式,接着写状态模式;在开始接触这两个模式的时候我也很疑惑,这两个设计模式很相似,用法也很类似。好一段时间我都没有区分这两者的区别。在使用的时候也不知道怎么选择,后来慢慢的加深理解也就总结出规律了。先看看状态模式的经典结构
状态模式介绍
《Android源码设计模式解析与实践》
定义
当一个对象的内在状态改变时容许改变其行为,这个对象开起来像是改变了其类
使用场景
1)一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为
2)代码包含大量与对象状态有关的条件语句,
状态模式将每个条件分支放入一个独立的类中,这使得你可以根据对象自身的情况将对象的状态作为一个对象,这一对象可以不依赖于其他对象而独立变化,这样通过多态来去除过多的、重复的if-else等分支语句
UML类图
《大话设计模式》
定义:
当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类(和《Android源码设计模式解析与实践》定义相同)
状态模式主要解决的是当控制一个对象状态转换的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类当中,可以把复杂的判断逻辑简化
策略模式VS状态模式
状态模式和策略模式的结构几乎完全一样,但它们的目的、本质却完全不一样。状态模式的行为是平行的、不可替换的,策略模式的行为是彼此独立、可相互替换的。用一句话来表述,状态模式把对象的行为包装在不同的状态对象里,每个状态对象都有一个共同的抽象状态基类。状态模式的意图是让一个对象在其内部状态改变的时候,其行为也随之改变–《Android源码设计模式解析与实践》
Glide状态模式应用SingleRequest.Status
包路径:com.bumptech.glide.request.SingleRequest.Status
将资源加载到给定目标中的请求。
/**
* A {@link Request} that loads a {@link com.bumptech.glide.load.engine.Resource} into a given
* {@link Target}.
*
* @param <R> The type of the resource that will be transcoded from the loaded resource.
*/
将资源加载到给定目标中的请求。
public final class SingleRequest<R> implements Request, SizeReadyCallback, ResourceCallback {
/** Tag for logging internal events, not generally suitable for public use. */
private static final String TAG = "GlideRequest";
/** Tag for logging externally useful events (request completion, timing etc). */
private static final String GLIDE_TAG = "Glide";
private static final boolean IS_VERBOSE_LOGGABLE = Log.isLoggable(TAG, Log.VERBOSE);
private int cookie;
private enum Status {
/** Created but not yet running. */
已创建但尚未运行。
PENDING,
/** In the process of fetching media. */
在获取媒体的过程中。
RUNNING,
/** Waiting for a callback given to the Target to be called to determine target dimensions. */
等待给定给Target的回调被调用以确定目标维度。
WAITING_FOR_SIZE,
/** Finished loading media successfully. */
成功加载媒体。
COMPLETE,
/** Failed to load media, may be restarted. */
加载介质失败,可能被重新启动。
FAILED,
/** Cleared by the user with a placeholder set, may be restarted. */
使用占位符设置的用户清除,可能会重新启动。
CLEARED,
}
Status.PENDING
// We are in fact locking on the same lock that will be used for all subsequent method calls.
@SuppressWarnings("GuardedBy")
private SingleRequest(
Context context,
GlideContext glideContext,
@NonNull Object requestLock,
@Nullable Object model,
Class<R> transcodeClass,
BaseRequestOptions<?> requestOptions,
int overrideWidth,
int overrideHeight,
Priority priority,
Target<R> target,
@Nullable RequestListener<R> targetListener,
@Nullable List<RequestListener<R>> requestListeners,
RequestCoordinator requestCoordinator,
Engine engine,
TransitionFactory<? super R> animationFactory,
Executor callbackExecutor) {
this.requestLock = requestLock;
this.context = context;
this.glideContext = glideContext;
this.model = model;
this.transcodeClass = transcodeClass;
this.requestOptions = requestOptions;
//。。。
status = Status.PENDING;
。。。
}
}
Status.RUNNING
@Override
public void begin() {
synchronized (requestLock) {
assertNotCallingCallbacks();
stateVerifier.throwIfRecycled();
startTime = LogTime.getLogTime();
....
if (status == Status.RUNNING) {
throw new IllegalArgumentException("Cannot restart a running request");
}
// If we're restarted after we're complete (usually via something like a notifyDataSetChanged
// that starts an identical request into the same Target or View), we can simply use the
// resource and size we retrieved the last time around and skip obtaining a new size, starting
// a new load etc. This does mean that users who want to restart a load because they expect
// that the view size has changed will need to explicitly clear the View or Target before
// starting the new load.
if (status == Status.COMPLETE) {
onResourceReady(
resource, DataSource.MEMORY_CACHE, /* isLoadedFromAlternateCacheKey= */ false);
return;
}
// Restarts for requests that are neither complete nor running can be treated as new requests
// and can run again from the beginning.
cookie = GlideTrace.beginSectionAsync(TAG);
status = Status.WAITING_FOR_SIZE;
if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
onSizeReady(overrideWidth, overrideHeight);
} else {
target.getSize(this);
}
if ((status == Status.RUNNING || status == Status.WAITING_FOR_SIZE)
&& canNotifyStatusChanged()) {
target.onLoadStarted(getPlaceholderDrawable());
}
if (IS_VERBOSE_LOGGABLE) {
logV("finished run method in " + LogTime.getElapsedMillis(startTime));
}
}
}
@Override
public boolean isRunning() {
synchronized (requestLock) {
return status == Status.RUNNING || status == Status.WAITING_FOR_SIZE;
}
}
/** A callback method that should never be invoked directly. */
@Override
public void onSizeReady(int width, int height) {
stateVerifier.throwIfRecycled();
synchronized (requestLock) {
if (IS_VERBOSE_LOGGABLE) {
logV("Got onSizeReady in " + LogTime.getElapsedMillis(startTime));
}
if (status != Status.WAITING_FOR_SIZE) {
return;
}
status = Status.RUNNING;
...
// This is a hack that's only useful for testing right now where loads complete synchronously
// even though under any executor running on any thread but the main thread, the load would
// have completed asynchronously.
if (status != Status.RUNNING) {
loadStatus = null;
}
if (IS_VERBOSE_LOGGABLE) {
logV("finished onSizeReady in " + LogTime.getElapsedMillis(startTime));
}
}
Status.WAITING_FOR_SIZE
@Override
public void begin() {
synchronized (requestLock) {
assertNotCallingCallbacks();
stateVerifier.throwIfRecycled();
startTime = LogTime.getLogTime();
if (model == null) {
if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
width = overrideWidth;
height = overrideHeight;
}
// Only log at more verbose log levels if the user has set a fallback drawable, because
// fallback Drawables indicate the user expects null models occasionally.
int logLevel = getFallbackDrawable() == null ? Log.WARN : Log.DEBUG;
onLoadFailed(new GlideException("Received null model"), logLevel);
return;
}
if (status == Status.RUNNING) {
throw new IllegalArgumentException("Cannot restart a running request");
}
// If we're restarted after we're complete (usually via something like a notifyDataSetChanged
// that starts an identical request into the same Target or View), we can simply use the
// resource and size we retrieved the last time around and skip obtaining a new size, starting
// a new load etc. This does mean that users who want to restart a load because they expect
// that the view size has changed will need to explicitly clear the View or Target before
// starting the new load.
if (status == Status.COMPLETE) {
onResourceReady(
//......
status = Status.WAITING_FOR_SIZE;
// ....
if ((status == Status.RUNNING || status == Status.WAITING_FOR_SIZE)
&& canNotifyStatusChanged()) {
target.onLoadStarted(getPlaceholderDrawable());
}
if (IS_VERBOSE_LOGGABLE) {
logV("finished run method in " + LogTime.getElapsedMillis(startTime));
}
}
}
@Override
public boolean isRunning() {
synchronized (requestLock) {
return status == Status.RUNNING || status == Status.WAITING_FOR_SIZE;
}
}
/** A callback method that should never be invoked directly. */
@Override
public void onSizeReady(int width, int height) {
//...
if (status != Status.WAITING_FOR_SIZE) {
return;
}
status = Status.RUNNING;
//...
Status.COMPLETE
public void begin() {
// If we're restarted after we're complete (usually via something like a notifyDataSetChanged
// that starts an identical request into the same Target or View), we can simply use the
// resource and size we retrieved the last time around and skip obtaining a new size, starting
// a new load etc. This does mean that users who want to restart a load because they expect
// that the view size has changed will need to explicitly clear the View or Target before
// starting the new load.
if (status == Status.COMPLETE) {
onResourceReady(
resource, DataSource.MEMORY_CACHE, /* isLoadedFromAlternateCacheKey= */ false);
return;
}
@Override
public boolean isComplete() {
synchronized (requestLock) {
return status == Status.COMPLETE;
}
}
@Override
public boolean isAnyResourceSet() {
synchronized (requestLock) {
return status == Status.COMPLETE;
}
}
/** A callback method that should never be invoked directly. */
@SuppressWarnings("unchecked")
@Override
public void onResourceReady(
....
if (!canSetResource()) {
toRelease = resource;
this.resource = null;
// We can't put the status to complete before asking canSetResource().
status = Status.COMPLETE;
....
/**
* Internal {@link #onResourceReady(Resource, DataSource, boolean)} where arguments are known to
* be safe.
*
* @param resource original {@link Resource}, never <code>null</code>
* @param result object returned by {@link Resource#get()}, checked for type and never <code>null
* </code>
*/
// We're using experimental APIs...
@SuppressWarnings({"deprecation", "PMD.UnusedFormalParameter"})
@GuardedBy("requestLock")
private void onResourceReady(
Resource<R> resource, R result, DataSource dataSource, boolean isAlternateCacheKey) {
// We must call isFirstReadyResource before setting status.
boolean isFirstResource = isFirstReadyResource();
status = Status.COMPLETE;
this.resource = resource;
...
Status.FAILED
private void onLoadFailed(GlideException e, int maxLogLevel) {
stateVerifier.throwIfRecycled();
synchronized (requestLock) {
e.setOrigin(requestOrigin);
int logLevel = glideContext.getLogLevel();
if (logLevel <= maxLogLevel) {
Log.w(
GLIDE_TAG, "Load failed for " + model + " with size [" + width + "x" + height + "]", e);
if (logLevel <= Log.INFO) {
e.logRootCauses(GLIDE_TAG);
}
}
loadStatus = null;
....
status = Status.FAILED;
....
Status.CLEARED
/**
* Cancels the current load if it is in progress, clears any resources held onto by the request
* and replaces the loaded resource if the load completed with the placeholder.
*
* <p>Cleared requests can be restarted with a subsequent call to {@link #begin()}
*
* @see #cancel()
*/
@Override
public void clear() {
Resource<R> toRelease = null;
synchronized (requestLock) {
assertNotCallingCallbacks();
stateVerifier.throwIfRecycled();
if (status == Status.CLEARED) {
return;
}
cancel();
// Resource must be released before canNotifyStatusChanged is called.
//.................
status = Status.CLEARED;
//.............
@Override
public boolean isCleared() {
synchronized (requestLock) {
return status == Status.CLEARED;
}
}
总结
之前的篇章作者都是讲结构很少涉及方法里的具体实现,这是因为具体涉及到业务了,如果不了解业务那理解上就有点障碍。状态模式这章之所以涉及到实现细节是因为这个状态模式的实现选择了枚举的方式,在方法里面进行逻辑业务实现。这个使用方式也是作者本人经常用的。状态模式在其他行业里有状态机的别称。不过本质上结构是一样的,就是具体的实现和行业应用场景有关。如果没有记错是硬件开发方面的说法。
在实际的项目应用开发中,作者在使用状态模式解决过复杂的if-else业务重构,类比同一应用场景下bug明显减少,代码质量明显提高。这个是测试人员反馈的测试结果。由此和测试人员关系很融洽。第二次是公司有一个项目被客户方给整成捆绑合同有一个需求迟迟解决不了,导致这个项目接近半年没有交付,由此导致销售总监、销售人员、部门老大、部门开发经理压力很大。为了实现这个需求不得不加班加点的开会写解决方案,在加上第三方硬件供应商的协调增加了实现的复杂性。。。在部门经理把这个需求转交给作者负责后,经过一个星期的设计,一个星期的调试开发。最后采用了状态模式来解决的。当然细节比介绍的状态模式复杂。不过主体使用的状态模式结构。如此花了半个月基本满足了客户的需求。也因此在开发团队中加分不少。
也是因为作者亲身经历,所以对状态模式感触很深。后来的一些项目也使用状态模式提高了开发代码质量和降低迭代扩展维护的难度。熟练的使用状态模式加深对面向对象编程的理解和应用。对于理解项目的模块分割和模块设计都有很好的促进作用。
总之作为开发掌握状态模式会有很多意想不到的好处。
自研产品推荐
历时一年半多开发终于smartApi-v1.0.0版本在2023-09-15晚十点正式上线
smartApi是一款对标国外的postman的api调试开发工具,由于开发人力就作者一个所以人力有限,因此v1.0.0版本功能进行精简,大功能项有:
- api参数填写
- api请求响应数据展示
- PDF形式的分享文档
- Mock本地化解决方案
- api列表数据本地化处理
- 再加上UI方面的打磨
为了更好服务大家把之前的公众号和软件激活结合,如有疑问请大家反馈到公众号即可,下个版本30%以上的更新会来自公众号的反馈。
嗯!先解释不上服务端原因,API调试工具的绝大多数时候就是一个数据模型、数据处理、数据模型理解共识的问题解决工具,所以作者结合自己十多年开发使用的一些痛点来打造的,再加上服务端开发一般是面向企业的,作者目前没有精力和时间去打造企业服务。再加上没有资金投入所以服务端开发会滞后,至于什么时候会进行开发,这个要看募资情况和用户反馈综合考虑。虽然目前国内有些比较知名的api工具了,但作者使用后还是觉得和实际使用场景不符。如果有相关吐槽也可以在作者的公众号里反馈蛤!
下面是一段smartApi使用介绍: