轉載請標明出處,維權必究: https://www.cnblogs.com/tangZH/p/12543154.html Glide作為一個強大的圖片載入框架,已經被android官方使用,所以,明白Glide的載入流程以及原理對加深我們對glide的理解是很重要的。 本文基於glide 4.11 ...
轉載請標明出處,維權必究: https://www.cnblogs.com/tangZH/p/12543154.html
Glide作為一個強大的圖片載入框架,已經被android官方使用,所以,明白Glide的載入流程以及原理對加深我們對glide的理解是很重要的。
本文基於glide 4.11
Glide.with(this).load("").into(new ImageView(this));
我們從這一句入手,上次我們看了Glide的初始化過程,也就是Glide.with(this)這個方法。現在我們來看into方法。
@NonNull public ViewTarget<ImageView, TranscodeType> into(@NonNull ImageView view) { Util.assertMainThread(); //檢查view是否為null Preconditions.checkNotNull(view); //根據view.getScaleType()設置不同的transform變換,這個transform變換我們單獨講 BaseRequestOptions<?> requestOptions = this; if (!requestOptions.isTransformationSet() && requestOptions.isTransformationAllowed() && view.getScaleType() != null) { // Clone in this method so that if we use this RequestBuilder to load into a View and then // into a different target, we don't retain the transformation applied based on the previous // View's scale type. switch (view.getScaleType()) { case CENTER_CROP: requestOptions = requestOptions.clone().optionalCenterCrop(); break; case CENTER_INSIDE: requestOptions = requestOptions.clone().optionalCenterInside(); break; case FIT_CENTER: case FIT_START: case FIT_END: requestOptions = requestOptions.clone().optionalFitCenter(); break; case FIT_XY: requestOptions = requestOptions.clone().optionalCenterInside(); break; case CENTER: case MATRIX: default: // Do nothing. } } return into( //根據transcodeClass的類型構造不同的Target glideContext.buildImageViewTarget(view, transcodeClass), /*targetListener=*/ null, requestOptions, Executors.mainThreadExecutor()); }
構建不同的target
glideContext.buildImageViewTarget(view, transcodeClass),跟著代碼點進去,最後跟蹤到了這裡:
public <Z> ViewTarget<ImageView, Z> buildTarget( @NonNull ImageView view, @NonNull Class<Z> clazz) { if (Bitmap.class.equals(clazz)) { return (ViewTarget<ImageView, Z>) new BitmapImageViewTarget(view); } else if (Drawable.class.isAssignableFrom(clazz)) { return (ViewTarget<ImageView, Z>) new DrawableImageViewTarget(view); } else { throw new IllegalArgumentException( "Unhandled class: " + clazz + ", try .as*(Class).transcode(ResourceTranscoder)"); } }
如果說我們最終要將資源解碼為bitmap,那麼就構造BitmapImageViewTarget,如果要將資源解碼為Drawable,那麼就構造DrawableImageViewTarget。
如果你在使用Glide載入圖片的時候調用了asBitmap()方法,那麼這裡就會構建出BitmapImageViewTarget對象,否則的話構建的都是DrawableImageViewTarget對象。 target裡面有一些方法,比如失敗的回調,設置資源等等。 接下來繼續看代碼,會調用下麵這個方法。
private <Y extends Target<TranscodeType>> Y into( @NonNull Y target, @Nullable RequestListener<TranscodeType> targetListener, BaseRequestOptions<?> options, Executor callbackExecutor) { Preconditions.checkNotNull(target); //檢測是否已經調用過load方法 if (!isModelSet) { throw new IllegalArgumentException("You must call #load() before calling #into()"); } //構造request Request request = buildRequest(target, targetListener, options, callbackExecutor); //獲取改target是否已經有綁定的request Request previous = target.getRequest(); /** * 這裡修複了一個bug,詳見 https://github.com/bumptech/glide/issues/2270 */ if (request.isEquivalentTo(previous) && !isSkipMemoryCacheWithCompletePreviousRequest(options, previous)) { // If the request is completed, beginning again will ensure the result is re-delivered, // triggering RequestListeners and Targets. If the request is failed, beginning again will // restart the request, giving it another chance to complete. If the request is already // running, we can let it continue running without interruption. if (!Preconditions.checkNotNull(previous).isRunning()) { // Use the previous request rather than the new one to allow for optimizations like skipping // setting placeholders, tracking and un-tracking Targets, and obtaining View dimensions // that are done in the individual Request. previous.begin(); } return target; } requestManager.clear(target); //將該request設置給target target.setRequest(request); requestManager.track(target, request); return target; }
先看一下buildRequest(target, targetListener, options, callbackExecutor);做了什麼
追蹤進去,調用buildRequestRecursive方法。
然後主要是這兩個方法:
Request mainRequest =
buildThumbnailRequestRecursive(
requestLock,
target,
targetListener,
parentCoordinator,
transitionOptions,
priority,
overrideWidth,
overrideHeight,
requestOptions,
callbackExecutor);
Request errorRequest =
errorBuilder.buildRequestRecursive(
requestLock,
target,
targetListener,
errorRequestCoordinator,
errorBuilder.transitionOptions,
errorBuilder.getPriority(),
errorOverrideWidth,
errorOverrideHeight,
errorBuilder,
callbackExecutor);
最後設置給ErrorRequestCoordinator
errorRequestCoordinator.setRequests(mainRequest, errorRequest);
ErrorRequestCoordinator負責管理這些請求,如果請求失敗就運行錯誤的請求。
我們看這個方法:buildThumbnailRequestRecursive
private Request buildThumbnailRequestRecursive( Object requestLock, Target<TranscodeType> target, RequestListener<TranscodeType> targetListener, @Nullable RequestCoordinator parentCoordinator, TransitionOptions<?, ? super TranscodeType> transitionOptions, Priority priority, int overrideWidth, int overrideHeight, BaseRequestOptions<?> requestOptions, Executor callbackExecutor) { if (thumbnailBuilder != null) { // Recursive case: contains a potentially recursive thumbnail request builder. if (isThumbnailBuilt) { throw new IllegalStateException( "You cannot use a request as both the main request and a " + "thumbnail, consider using clone() on the request(s) passed to thumbnail()"); } TransitionOptions<?, ? super TranscodeType> thumbTransitionOptions = thumbnailBuilder.transitionOptions; // Apply our transition by default to thumbnail requests but avoid overriding custom options // that may have been applied on the thumbnail request explicitly. if (thumbnailBuilder.isDefaultTransitionOptionsSet) { thumbTransitionOptions = transitionOptions; } Priority thumbPriority = thumbnailBuilder.isPrioritySet() ? thumbnailBuilder.getPriority() : getThumbnailPriority(priority); int thumbOverrideWidth = thumbnailBuilder.getOverrideWidth(); int thumbOverrideHeight = thumbnailBuilder.getOverrideHeight(); if (Util.isValidDimensions(overrideWidth, overrideHeight) && !thumbnailBuilder.isValidOverride()) { thumbOverrideWidth = requestOptions.getOverrideWidth(); thumbOverrideHeight = requestOptions.getOverrideHeight(); } ThumbnailRequestCoordinator coordinator = new ThumbnailRequestCoordinator(requestLock, parentCoordinator); Request fullRequest = obtainRequest( requestLock, target, targetListener, requestOptions, coordinator, transitionOptions, priority, overrideWidth, overrideHeight, callbackExecutor); isThumbnailBuilt = true; // Recursively generate thumbnail requests. Request thumbRequest = thumbnailBuilder.buildRequestRecursive( requestLock, target, targetListener, coordinator, thumbTransitionOptions, thumbPriority, thumbOverrideWidth, thumbOverrideHeight, thumbnailBuilder, callbackExecutor); isThumbnailBuilt = false; coordinator.setRequests(fullRequest, thumbRequest); return coordinator; } else if (thumbSizeMultiplier != null) { // Base case: thumbnail multiplier generates a thumbnail request, but cannot recurse. ThumbnailRequestCoordinator coordinator = new ThumbnailRequestCoordinator(requestLock, parentCoordinator); Request fullRequest = obtainRequest( requestLock, target, targetListener, requestOptions, coordinator, transitionOptions, priority, overrideWidth, overrideHeight, callbackExecutor); BaseRequestOptions<?> thumbnailOptions = requestOptions.clone().sizeMultiplier(thumbSizeMultiplier); Request thumbnailRequest = obtainRequest( requestLock, target, targetListener, thumbnailOptions, coordinator, transitionOptions, getThumbnailPriority(priority), overrideWidth, overrideHeight, callbackExecutor); coordinator.setRequests(fullRequest, thumbnailRequest); return coordinator; } else { // Base case: no thumbnail. return obtainRequest( requestLock, target, targetListener, requestOptions, parentCoordinator, transitionOptions, priority, overrideWidth, overrideHeight, callbackExecutor); } }
首先對縮略圖及是否對Target設置參數的判斷(是否使用了thumbnail()方法和sizeMultiplier()方法),如果有使用thunmnail()方法,則生成原始圖片和縮略圖的請求,並由ThumbnailRequestCoordinator對象來協調管理,使用了sizeMultiplier()方法,則同樣的處理(前者遞歸的獲得縮略圖的Request,後者不遞歸),否則就只生成原始圖片的請求。
他們最終都會調用obtainRequest方法,追蹤進去可以發現該方法最終返回的是SingleRequest對象。初始化request的時候傳遞的參數很多:
public static <R> SingleRequest<R> obtain( Context context, GlideContext glideContext, Object requestLock, Object model, Class<R> transcodeClass, BaseRequestOptions<?> requestOptions, int overrideWidth, int overrideHeight, Priority priority, Target<R> target, RequestListener<R> targetListener, @Nullable List<RequestListener<R>> requestListeners, RequestCoordinator requestCoordinator, Engine engine, TransitionFactory<? super R> animationFactory, Executor callbackExecutor) {
1.GlideContext glideContext : 全局上下文
2.Object model :載入的資源類型
3.Class transcodeClass :轉換的類型
4.RequestOptions requestOptions:設置選項(包括:skipMemoryCache,errorDrawable,placeholder,timeoutOf,encodeFormatOf等等)
5.int overrideWidth:目標寬度在所需資源的像素點。
6.int overrideHeight:目標高度在所需資源的像素點。
7. Priority priority:載入的優先順序(IMMEDIATE,HIGH,NORMAL,LOW)
8.Target target:上面剛講過,綁定的target
9.RequestListener requestListener:請求載入時候的監聽器
10.RequestCoordinator requestCoordinator:請求協調器(用來協調具有相同Target的協調器)
11.Engine engine:負責啟動負載和管理活動和緩存資源。
12.TransitionFactory<? super R> animationFactory:一個工廠類,可以根據請求的狀態產生不同的轉換。
我們再回到into代碼中,獲取了request之後我們就要開始請求了。
我們看著一句requestManager.track(target, request);
synchronized void track(@NonNull Target<?> target, @NonNull Request request) { targetTracker.track(target); requestTracker.runRequest(request); }
runRequest就是執行請求的代碼:
/** Starts tracking the given request. */ public void runRequest(@NonNull Request request) { requests.add(request); if (!isPaused) { request.begin(); } else { request.clear(); if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Paused, delaying request"); } pendingRequests.add(request); } }
判斷Glide當前是不是處理暫停狀態,如果不是暫停狀態就調用Request的begin()方法來執行Request,否則的話就先將Request添加到待執行隊列裡面,等暫停狀態解除了之後再執行。
我們來看begin方法:
@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"); }
如果說這個資源已經被載入過了,那麼我們直接調用onResourceReady // 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); return; } // Restarts for requests that are neither complete nor running can be treated as new requests // and can run again from the beginning. 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)); } } }
如果model為null,說明我們沒有調用load方法,這時候會回調onLoadFailed,將status設置為Status.FAILED,然後調用setErrorPlaceholder,這個方法裡面最終調用target.onLoadFailed(error);將資源置空,然後顯示錯誤圖片。
@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 == SingleRequest.Status.RUNNING) { throw new IllegalArgumentException("Cannot restart a running request"); } /** * 如果完成後重新啟動(通常是通過notifyDataSetChanged之類的方法 * 將相同的請求發送到相同的Target或View),我們可以簡單地使用 * 我們最後一次檢索的資源和大小,然後跳過獲取新大小的步驟, * 不用開始一個新的載入。這確實意味著要重新載入的用戶,因為他們 * 更改視圖大小,那麼需要先明確清除view和target,然後 * 開始新的載入。 */ if (status == SingleRequest.Status.COMPLETE) { onResourceReady(resource, DataSource.MEMORY_CACHE); return; } // Restarts for requests that are neither complete nor running can be treated as new requests // and can run again from the beginning. status = SingleRequest.Status.WAITING_FOR_SIZE; /** * 這裡會判斷Util.isValidDimensions(overrideWidth, overrideHeight) * 如果你在使用時候調用了override() API為圖片指定了一個固定的寬高,就會按照你給定的去載入;第二種情況是沒有給定的情況, * 那麼target.getSize()方法的內部會根據ImageView的layout_width和layout_height值做一系列的計算,來算出圖片應該的寬高, * 具體計算就在getSize裡面 * 但是不管怎樣,最後都會調用onSizeReady()。 */ if (Util.isValidDimensions(overrideWidth, overrideHeight)) { onSizeReady(overrideWidth, overrideHeight); } else { target.getSize(this); } if ((status == SingleRequest.Status.RUNNING || status == SingleRequest.Status.WAITING_FOR_SIZE) && canNotifyStatusChanged()) { target.onLoadStarted(getPlaceholderDrawable()); } if (IS_VERBOSE_LOGGABLE) { logV("finished run method in " + LogTime.getElapsedMillis(startTime)); } } }
我們進去onSizeReady看看
@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 != SingleRequest.Status.WAITING_FOR_SIZE) { return; } status = SingleRequest.Status.RUNNING; float sizeMultiplier = requestOptions.getSizeMultiplier(); this.width = maybeApplySizeMultiplier(width, sizeMultiplier); this.height = maybeApplySizeMultiplier(height, sizeMultiplier); if (IS_VERBOSE_LOGGABLE) { logV("finished setup for calling load in " + LogTime.getElapsedMillis(startTime)); } loadStatus = engine.load( glideContext, model, requestOptions.getSignature(), this.width, this.height, requestOptions.getResourceClass(), transcodeClass, priority, requestOptions.getDiskCacheStrategy(), requestOptions.getTransformations(), requestOptions.isTransformationRequired(), requestOptions.isScaleOnlyOrNoTransform(), requestOptions.getOptions(), requestOptions.isMemoryCacheable(), requestOptions.getUseUnlimitedSourceGeneratorsPool(), requestOptions.getUseAnimationPool(), requestOptions.getOnlyRetrieveFromCache(), this, callbackExecutor); // 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 != SingleRequest.Status.RUNNING) { loadStatus = null; } if (IS_VERBOSE_LOGGABLE) { logV("finished onSizeReady in " + LogTime.getElapsedMillis(startTime)); } } }
主要的代碼是engine.load。
/** * /** * 所有的請求流程都如下: * 1.檢查記憶體緩存並提供緩存的資源 * 2.檢查當前使用的資源,並返回當前的活躍資源 * 3.檢查當前的載入進度,並將cb添加到正在進行的載入進度中 * 4.開始一個新的載入 * * @param glideContext * @param model * @param signature * @param width * @param height * @param resourceClass * @param transcodeClass * @param priority * @param diskCacheStrategy * @param transformations * @param isTransformationRequired * @param isScaleOnlyOrNoTransform * @param options * @param isMemoryCacheable * @param useUnlimitedSourceExecutorPool * @param useAnimationPool * @param onlyRetrieveFromCache * @param cb * @param callbackExecutor * @param <R> * @return */ public <R> Engine.LoadStatus load( GlideContext glideContext, Object model, Key signature, int width, int height, Class<?> resourceClass, Class<R> transcodeClass, Priority priority, DiskCacheStrategy diskCacheStrategy, Map<Class<?>, Transformation<?>> transformations, boolean isTransformationRequired, boolean isScaleOnlyOrNoTransform, Options options, boolean isMemoryCacheable, boolean useUnlimitedSourceExecutorPool, boolean useAnimationPool, boolean onlyRetrieveFromCache, ResourceCallback cb, Executor callbackExecutor) { long startTime = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0; //構造一個key EngineKey key = keyFactory.buildKey( model, signature, width, height, transformations, resourceClass, transcodeClass, options); EngineResource<?> memoryResource; synchronized (this) { //通過這個key去緩存中看是不是存在資源,loadFromMemory裡面會先去活躍資源緩存池中獲取, // 沒有的話再去記憶體緩存中獲取,活躍資源即現在正在被其他組件使用的資源。 memoryResource = loadFromMemory(key, isMemoryCacheable, startTime); if (memoryResource == null) { return waitForExistingOrStartNewJob( glideContext, model, signature, width, height, resourceClass, transcodeClass, priority, diskCacheStrategy, transformations, isTransformationRequired, isScaleOnlyOrNoTransform, options, isMemoryCacheable, useUnlimitedSourceExecutorPool, useAnimationPool, onlyRetrieveFromCache, cb, callbackExecutor, key, startTime); } } // Avoid calling back while holding the engine lock, doing so makes it easier for callers to // deadlock. cb.onResourceReady(memoryResource, DataSource.MEMORY_CACHE); return null; }
如果找得到資源,那麼就回調cb.onResourceReady,不然的話會走waitForExistingOrStartNewJob。
我們進去看一下:
private <R> Engine.LoadStatus waitForExistingOrStartNewJob( GlideContext glideContext, Object model, Key signature, int width, int height, Class<?> resourceClass, Class<R> transcodeClass, Priority priority, DiskCacheStrategy diskCacheStrategy, Map<Class<?>, Transformation<?>> transformations, boolean isTransformationRequired, boolean isScaleOnlyOrNoTransform, Options options, boolean isMemoryCacheable, boolean useUnlimitedSourceExecutorPool, boolean useAnimationPool, boolean onlyRetrieveFromCache, ResourceCallback cb, Executor callbackExecutor, EngineKey key, long startTime) { //通過key獲取EngineJob,EngineJob負責開啟線程非同步載入。 EngineJob<?> current = jobs.get(key, onlyRetrieveFromCache); if (current != null) { current.addCallback(cb, callbackExecutor); if (VERBOSE_IS_LOGGABLE) { logWithTimeAndKey("Added to existing load", startTime, key); } return new Engine.LoadStatus(cb, current); } //沒有EngineJob則構建一個 EngineJob<R> engineJob = engineJobFactory.build( key, isMemoryCacheable, useUnlimitedSourceExecutorPool, useAnimationPool, onlyRetrieveFromCache); //負責給圖片解碼等一些複雜操作 DecodeJob<R> decodeJob = decodeJobFactory.build( glideContext, model, key, signature, width, height, resourceClass, transcodeClass, priority, diskCacheStrategy, transformations, isTransformationRequired, isScaleOnlyOrNoTransform, onlyRetrieveFromCache, options, engineJob); jobs.put(key, engineJob); engineJob.addCallback(cb, callbackExecutor); //運行 engineJob.start(decodeJob); if (VERBOSE_IS_LOGGABLE) { logWithTimeAndKey("Started new load", startTime, key); } return new Engine.LoadStatus(cb, engineJob); }
public synchronized void start(DecodeJob<R> decodeJob) { this.decodeJob = decodeJob; GlideExecutor executor = decodeJob.willDecodeFromCache() ? diskCacheExecutor : getActiveSourceExecutor(); executor.execute(decodeJob); }
如果要從磁碟緩存中去解碼的話,就獲取diskCacheExecutor,否則就用針對原始資源的一個執行器。
在executor.execute(decodeJob)之後便切換到子線程了,我們到DecodeJob裡面去看一下。
@Override public void run() { // This should be much more fine grained, but since Java's thread pool implementation silently // swallows all otherwise fatal exceptions, this will at least make it obvious to developers // that something is failing. GlideTrace.beginSectionFormat("DecodeJob#run(model=%s)", model); // Methods in the try statement can invalidate currentFetcher, so set a local variable here to // ensure that the fetcher is cleaned up either way. DataFetcher<?> localFetcher = currentFetcher; try { if (isCancelled) { notifyFailed(); return; } runWrapped(); } catch (CallbackException e) { // If a callback not controlled by Glide throws an exception, we should avoid the Glide // specific debug logic below. throw e; } catch (Throwable t) { // Catch Throwable and not Exception to handle OOMs. Throwables are swallowed by our // usage of .submit() in GlideExecutor so we're not silently hiding crashes by doing this. We // are however ensuring that our callbacks are always notified when a load fails. Without this // notification, uncaught throwables never notify the corresponding callbacks, which can cause // loads to silently hang forever, a case that's especially bad for users using Futures on // background threads. if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d( TAG, "DecodeJob threw unexpectedly" + ", isCancelled: " + isCancelled + ", stage: " + stage, t); } // When we're encoding we've already notified our callback and it isn't safe to do so again. if (stage != Stage.ENCODE) { throwables.add(t); notifyFailed(); } if (!isCancelled) { throw t; } throw t; } finally { // Keeping track of the fetcher here and calling cleanup is excessively paranoid, we call // close in all cases anyway. if (localFetcher != null) { localFetcher.cleanup(); } GlideTrace.endSection(); } }
主要是runWrapped();
private void runWrapped() { switch (runReason) { case INITIALIZE: stage = getNextStage(Stage.INITIALIZE); currentGenerator = getNextGenerator(); runGenerators(); break; case SWITCH_TO_SOURCE_SERVICE: runGenerators(); break; case DECODE_DATA: decodeFromRetrievedData(); break; default: throw new IllegalStateException("Unrecognized run reason: " + runReason); } }
當INITIALIZE或者SWITCH_TO_SOURCE_SERVICE的時候,走runGenerators()。這兩種是沒有緩存的情況下。
runGenerators():
private void runGenerators() { currentThread = Thread.currentThread(); startFetchTime = LogTime.getLogTime(); boolean isStarted = false; while (!isCancelled && currentGenerator != null && !(isStarted = currentGenerator.startNext())) { stage = getNextStage(stage); currentGenerator = getNextGenerator(); if (stage == Stage.SOURCE) { reschedule(); return; } }