Glide Source Code Architecture and Kernel Principles
In the previous article, we explored Glide's engineering practices in production environments. Now, we will dive deep into Glide's source code to uncover the hardcore architectural designs behind the simple Glide.with().load().into() API.
We will focus on four core mechanisms:
- Lifecycle Binding: How does Glide automatically pause and resume requests without causing memory leaks?
- Multi-Tier Caching: How do ActiveResources, MemoryCache, and DiskCache collaborate?
- Task Scheduling: How do
EngineJobandDecodeJobimplement complex thread switching and state machines? - Bitmap Pooling: How does Glide reuse memory to avoid garbage collection (GC) thrashing?
I. Overall Architecture: The Anatomy of Glide
Before delving into the code, let's understand Glide's overarching architectural hierarchy. Glide is designed with a high degree of modularity, clearly separating the API layer, task orchestration layer, and execution engine.
┌────────────────────────────────────────────────────────────────────────┐
│ API Layer │
│ Glide.with(context) .load(url) .into(imageView) │
│ (RequestManagerRetriever) (RequestBuilder) (Target / Request) │
└───────────────────────────────────┬────────────────────────────────────┘
│ Generates SingleRequest
▼
┌────────────────────────────────────────────────────────────────────────┐
│ Engine (Orchestration) │
│ │
│ 1. Check ActiveResources (WeakReference) │
│ 2. Check MemoryCache (LruCache) │
│ 3. Check EngineJob (Deduplication for identical ongoing requests) │
│ 4. Submit new EngineJob & DecodeJob │
└───────────────────────────────────┬────────────────────────────────────┘
│ Submit task to Thread Pool
▼
┌────────────────────────────────────────────────────────────────────────┐
│ DecodeJob (State Machine) │
│ │
│ State 1: INITIALIZE │
│ State 2: RESOURCE_CACHE (Fetch decoded image from disk) │
│ State 3: DATA_CACHE (Fetch original data from disk) │
│ State 4: SOURCE (Fetch from Network/Local File) │
└───────────────────────────────────┬────────────────────────────────────┘
│ Delegate to Loaders
▼
┌────────────────────────────────────────────────────────────────────────┐
│ Data Providers (ModelLoader) │
│ │
│ HttpUrlFetcher (OkHttp/HttpURLConnection) │
│ FileFetcher │
│ AssetFetcher │
└────────────────────────────────────────────────────────────────────────┘
II. Lifecycle Binding Mechanism
One of Glide's most elegant features is its automatic lifecycle management. If an Activity is destroyed while an image is still downloading, the request is automatically canceled, preventing memory leaks and NullPointerExceptions.
2.1 The Magic of Glide.with()
When you call Glide.with(Activity), Glide creates a hidden, UI-less Fragment and injects it into your Activity.
// RequestManagerRetriever.java
public RequestManager get(@NonNull Activity activity) {
if (Util.isOnBackgroundThread()) {
return get(activity.getApplicationContext()); // Fallback to Application context on background threads
}
assertNotDestroyed(activity);
android.app.FragmentManager fm = activity.getFragmentManager();
// 1. Get or create the hidden Fragment
return fragmentGet(activity, fm, /*parentHint=*/ null, isActivityVisible(activity));
}
private RequestManager fragmentGet(
@NonNull Context context,
@NonNull android.app.FragmentManager fm,
@Nullable android.app.Fragment parentHint,
boolean isParentVisible) {
// 2. Look for the hidden fragment
RequestManagerFragment current = getRequestManagerFragment(fm, parentHint, isParentVisible);
// 3. Get the RequestManager bound to it
RequestManager requestManager = current.getRequestManager();
if (requestManager == null) {
Glide glide = Glide.get(context);
// 4. Create RequestManager and pass the Fragment's Lifecycle
requestManager =
factory.build(
glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);
current.setRequestManager(requestManager);
}
return requestManager;
}
2.2 The Hidden RequestManagerFragment
The injected RequestManagerFragment hooks into the host Activity's lifecycle callbacks:
// RequestManagerFragment.java
public class RequestManagerFragment extends Fragment {
// ActivityFragmentLifecycle is an implementation of Glide's internal Lifecycle interface
private final ActivityFragmentLifecycle lifecycle;
@Override
public void onStart() {
super.onStart();
lifecycle.onStart(); // Propagate to RequestManager
}
@Override
public void onStop() {
super.onStop();
lifecycle.onStop();
}
@Override
public void onDestroy() {
super.onDestroy();
lifecycle.onDestroy();
}
}
The RequestManager implements the LifecycleListener interface. When lifecycle.onStop() is invoked, the RequestManager pauses all ongoing Requests; when onDestroy() is invoked, it clears all requests and releases resources.
Design Insight: Why use a hidden Fragment?
This is a classic Android architectural pattern (used by ViewModel before Lifecycle components existed). By tying an invisible Fragment to an Activity/Fragment, the invisible Fragment inherently syncs with the host's lifecycle. Glide abstracts Android's complex lifecycles into a uniform, decoupled Lifecycle interface.
III. Execution Engine: The Request Flow
When you call into(imageView), Glide constructs a SingleRequest and submits it to the Engine. The Engine is the core orchestrator.
3.1 Engine.load() - The Entry Point
// Engine.java
public <R> LoadStatus load(...) {
// 1. Generate a unique cache Key for the request (includes URL, width, height, transformations, signatures, etc.)
EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations, ...);
// 2. Check ActiveResources (Level 1 Cache)
EngineResource<?> active = loadFromActiveResources(key);
if (active != null) {
cb.onResourceReady(active, DataSource.MEMORY_CACHE);
return null;
}
// 3. Check MemoryCache (Level 2 Cache)
EngineResource<?> cached = loadFromCache(key);
if (cached != null) {
cb.onResourceReady(cached, DataSource.MEMORY_CACHE);
return null;
}
// 4. Check if an identical request is already running (Deduplication)
EngineJob<?> current = jobs.get(key, onlyRetrieveFromCache);
if (current != null) {
// Just attach the callback to the existing EngineJob, do not initiate a new download
current.addCallback(cb, callbackExecutor);
return new LoadStatus(cb, current);
}
// 5. Build a new EngineJob (Task Manager) and DecodeJob (Actual Worker)
EngineJob<R> engineJob =
engineJobFactory.build(key, isMemoryCacheable, useUnlimitedSourceExecutorPool, ...);
DecodeJob<R> decodeJob =
decodeJobFactory.build(
glideContext, model, key, signature, width, height, resourceClass, engineJob);
jobs.put(key, engineJob);
// 6. Start execution
engineJob.addCallback(cb, callbackExecutor);
engineJob.start(decodeJob);
return new LoadStatus(cb, engineJob);
}
The sequence is strictly prioritized: ActiveResources -> LruCache -> EngineJob Deduplication -> DecodeJob Execution.
IV. Multi-Tier Caching Architecture
Glide's caching system is renowned for its sophistication, designed specifically to minimize Android UI thread stutter and GC pauses.
4.1 Memory Cache Tier 1: ActiveResources
ActiveResources holds images that are currently visible on the screen.
// ActiveResources.java
final Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>();
synchronized void activate(Key key, EngineResource<?> resource) {
// Wrap the resource in a WeakReference.
// It also registers a ReferenceQueue to track when the resource is GC'd.
ResourceWeakReference toPut =
new ResourceWeakReference(
key, resource, resourceReferenceQueue, isActiveResourceRetentionAllowed);
ResourceWeakReference removed = activeEngineResources.put(key, toPut);
if (removed != null) {
removed.reset();
}
}
synchronized void deactivate(Key key) {
ResourceWeakReference removed = activeEngineResources.remove(key);
if (removed != null) {
removed.reset();
}
}
Why WeakReferences?
When an image is displayed in an ImageView, the ImageView holds a strong reference to the BitmapDrawable (which wraps the Bitmap). ActiveResources only holds a WeakReference to it. If the ImageView is destroyed or recycled (e.g., scrolled off-screen in a RecyclerView), the strong reference is lost. At the next GC cycle, the WeakReference will be collected, and Glide's ReferenceQueue thread will detect this, moving the EngineResource out of ActiveResources and into the LruResourceCache.
4.2 Memory Cache Tier 2: LruResourceCache
When an image is no longer actively displayed, it is pushed to the LruResourceCache.
// LruResourceCache.java (Extends LruCache)
public class LruResourceCache extends LruCache<Key, Resource<?>> implements MemoryCache {
@Override
protected void onItemEvicted(@NonNull Key key, @Nullable Resource<?> item) {
// When an item is evicted from the LruCache due to memory pressure,
// it notifies the listener to recycle the resource (e.g., returning the Bitmap to the BitmapPool).
if (listener != null && item != null) {
listener.onResourceRemoved(item);
}
}
}
Data Flow Summary:
- Load Image: Miss in Active, Miss in LRU -> Load from Disk/Network -> Save to Active -> Display.
- Scroll off-screen:
ImageViewreleases reference -> GC triggered ->ActiveResourcesloses reference -> MovesResourceintoLruResourceCache. - Scroll back on-screen: Found in
LruResourceCache-> Remove fromLruResourceCache-> Move back toActiveResources-> Display.
By strictly separating "In Use" (Active) and "Recently Used" (LRU), Glide prevents large, visible images from being prematurely evicted from the LRU cache just because a sudden burst of new, smaller images flooded the cache.
4.3 Disk Cache: Resource vs. Data
Glide implements a dual-layer disk cache strategy:
- RESOURCE_CACHE (Result Cache): Caches the final, transformed image (downsampled, cropped, rotated). This ensures blazing-fast reads for the UI without re-decoding.
- DATA_CACHE (Source Cache): Caches the pristine, original binary data fetched from the network.
When loading an image, Glide traverses these layers backward:
ActiveResources -> MemoryCache -> RESOURCE_CACHE -> DATA_CACHE -> Network/Source.
V. DecodeJob: The State Machine
DecodeJob implements Runnable and is responsible for actually fetching, decoding, and transforming the image on a background thread pool. Because it may need to switch between disk threads and network threads, it employs a State Machine pattern.
5.1 Stage Transitions
// DecodeJob.java
private enum Stage {
INITIALIZE,
RESOURCE_CACHE, // Check transformed disk cache
DATA_CACHE, // Check original disk cache
SOURCE, // Fetch from network/local file
ENCODE,
FINISHED
}
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);
}
}
private void runGenerators() {
boolean isStarted = false;
// Iterate through generators until one starts a load successfully or we finish all stages
while (!isCancelled
&& currentGenerator != null
&& !(isStarted = currentGenerator.startNext())) {
// If current generator (e.g. ResourceCacheGenerator) fails to find data, move to next stage
stage = getNextStage(stage);
currentGenerator = getNextGenerator();
if (stage == Stage.SOURCE) {
// Needs to fetch from source, re-schedule to the Source Executor
reschedule();
return;
}
}
}
Generators mapping:
Stage.RESOURCE_CACHE->ResourceCacheGenerator(Reads from DiskLruCache)Stage.DATA_CACHE->DataCacheGenerator(Reads from DiskLruCache, triggers decode/transform)Stage.SOURCE->SourceGenerator(Fetches via OkHttp/HttpURLConnection)
This state machine isolates the complex asynchronous fetching logic from the thread scheduling logic. If a cache miss occurs on the disk thread, it simply transitions the stage and potentially re-schedules itself onto the Source thread pool.
VI. Memory Optimization: BitmapPool
Android's GC behavior historically struggled with large Bitmap allocations, causing UI stutter. Glide mitigates this using a BitmapPool.
6.1 The LruBitmapPool
// LruBitmapPool.java
public class LruBitmapPool implements BitmapPool {
private final LruPoolStrategy strategy;
@Override
public Bitmap get(int width, int height, Bitmap.Config config) {
Bitmap result = getDirtyOrNull(width, height, config);
if (result != null) {
// Found a reusable Bitmap, clear its contents
result.eraseColor(Color.TRANSPARENT);
} else {
// Pool miss, allocate new Bitmap
result = Bitmap.createBitmap(width, height, config);
}
return result;
}
@Override
public void put(Bitmap bitmap) {
// When a Resource is evicted from LruResourceCache, its Bitmap is returned here
if (!bitmap.isMutable() || strategy.getSize(bitmap) > maxSize || ...) {
bitmap.recycle();
return;
}
strategy.put(bitmap);
}
}
How does strategy work?
Glide uses SizeConfigStrategy on modern Android versions. It prioritizes reusing a Bitmap that requires the exact same byte size, rather than requiring strict matching of width and height. For instance, a 100x100 ARGB_8888 (40KB) Bitmap can be reused to hold a 200x100 RGB_565 (40KB) image. This flexibility drastically increases the cache hit rate of the pool.
VII. Summary
Glide's source code is a masterclass in Android performance engineering. By dissecting its architecture, we can extract several highly valuable industrial-grade patterns:
| Engineering Problem | Glide's Solution | Architectural Pattern |
|---|---|---|
| Lifecycle Binding | Injecting a hidden Fragment | UI/Logic Decoupling |
| Preventing Redundant Network Calls | EngineJob Maps to deduplicate identical ongoing requests |
Request Coalescing |
| Preventing GC Thrashing | LruBitmapPool and ArrayPool |
Object Pooling / Flyweight |
| Protecting In-Use Images from Eviction | ActiveResources (WeakReferences) + LruCache |
Two-Tier Memory Hierarchy |
| Managing Complex Async Fallbacks | DecodeJob + DataFetcher Generators |
State Machine |
| Disk I/O Bottlenecks | Double Disk Cache (Resource + Source) | Space-Time Tradeoff |
Glide's complexity is fully justified by its objective: achieving buttery smooth scrolling in RecyclerViews under severe memory constraints. Understanding Glide's internals not only helps us debug memory leaks and cache misses but also provides a robust blueprint for writing any high-performance, asynchronous media pipeline in Android.