ㄷㅣㅆㅣ's Amusement

[Android/Java] Glide, source analysis -- 4. ModelLoader & DataFetcher 본문

Programming/Android

[Android/Java] Glide, source analysis -- 4. ModelLoader & DataFetcher

ㄷㅣㅆㅣ 2017. 1. 4. 14:08

[Android/Java] Glide, source analysis -- 4. ModelLoader & DataFetcher

반응형

Glide

ModelLoader & DataFetcher






앞서 3장에서 봤던 DecodeJob은 rungenerator()에서 다음과 같은 이유/단계로 동작했다.

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
/**
 * Why we're being executed again.
 */
private enum RunReason {
  /** The first time we've been submitted. */
  INITIALIZE,
  /**
   * We want to switch from the disk cache service to the source executor.
   */
  SWITCH_TO_SOURCE_SERVICE,
  /**
   * We retrieved some data on a thread we don't own and want to switch back to our thread to
   * process the data.
   */
  DECODE_DATA,
}
 
/**
 * Where we're trying to decode data from.
 */
private enum Stage {
  /** The initial stage. */
  INITIALIZE,
  /** Decode from a cached resource. */
  RESOURCE_CACHE,
  /** Decode from cached source data. */
  DATA_CACHE,
  /** Decode from retrieved source. */
  SOURCE,
  /** Encoding transformed resources after a successful load. */
  ENCODE,
  /** No more viable stages. */
  FINISHED,
}
cs


그렇다면 더 세부적으로, 캐쉬에서는 어떻게 가져오는지, 캐쉬에 없다면 어떻게 가져오는지, 그리고 가져온 후에는 어떠한 동작을 수행하는지에 대해서 알아본다.

다음은 DataFetcherGenerator의 자식 클래스인 ResourceCacheGenerator를 붙여놓았다.

위의 시퀀스 다이어그램의 1.startNext부터 코드레벨로 살펴보자. 이것은 3장에서 분석한대로 rungenerator()에 의해 호출된다.

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
resourceCacheGenerator.java

@Override
public boolean startNext() {
  List<Key> sourceIds = helper.getCacheKeys();
  if (sourceIds.isEmpty()) {
    return false;
  }
  List<Class<?>> resourceClasses = helper.getRegisteredResourceClasses();
  while (modelLoaders == null || !hasNextModelLoader()) {
    resourceClassIndex++;
    if (resourceClassIndex >= resourceClasses.size()) {
      sourceIdIndex++;
      if (sourceIdIndex >= sourceIds.size()) {
        return false;
      }
      resourceClassIndex = 0;
    }
 
    Key sourceId = sourceIds.get(sourceIdIndex);
    Class<?> resourceClass = resourceClasses.get(resourceClassIndex);
    Transformation<?> transformation = helper.getTransformation(resourceClass);
 
    currentKey = new ResourceCacheKey(sourceId, helper.getSignature(), helper.getWidth(),
        helper.getHeight(), transformation, resourceClass, helper.getOptions());
    cacheFile = helper.getDiskCache().get(currentKey);
    if (cacheFile != null) {
      this.sourceKey = sourceId;
      modelLoaders = helper.getModelLoaders(cacheFile);
      modelLoaderIndex = 0;
    }
  }
 
  loadData = null;
  boolean started = false;
  while (!started && hasNextModelLoader()) {
    ModelLoader<File, ?> modelLoader = modelLoaders.get(modelLoaderIndex++);
    loadData =
        modelLoader.buildLoadData(cacheFile, helper.getWidth(), helper.getHeight(),
            helper.getOptions());
    if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) {
      started = true;
      loadData.fetcher.loadData(helper.getPriority(), this);
    }
  }
 
  return started;
}
cs

 - 결론적으로는 DataFetcher의 loadData를 호출하여 데이터를 가져오고, 이것을 Decode하게된다.



 - DataFetcher는 Model에 따라서 Data를 가져오는 방법을 달리할 수 있도록 Glide.java에 Registry로 연결해둔대로 호출되며 각 fetcher들은 가져오는 장소만 다를 뿐 대체로 구조가 비슷하다.

 - 다음은 Fetcher의 자식클래스중 하나인 HttpUrlFetcher의 loadData()부분이다.  이름에서부터 유추해볼 수 있듯이 HTTP connection을 이용하여 서버에 있는 데이터를 가져오게된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// HttpUrlFetcher.java
 
@Override
public void loadData(Priority priority, DataCallback<super InputStream> callback) {
  long startTime = LogTime.getLogTime();
  final InputStream result;
  try {
    result = loadDataWithRedirects(glideUrl.toURL(), 0 /*redirects*/null /*lastUrl*/,
        glideUrl.getHeaders());
  } catch (IOException e) {
    if (Log.isLoggable(TAG, Log.DEBUG)) {
      Log.d(TAG, "Failed to load data for url", e);
    }
    callback.onLoadFailed(e);
    return;
  }
 
  if (Log.isLoggable(TAG, Log.VERBOSE)) {
    Log.v(TAG, "Finished http url fetcher fetch in " + LogTime.getElapsedMillis(startTime)
        + " ms and loaded " + result);
  }
  callback.onDataReady(result);
}
cs




<< 가져온 데이터 Decode시작 >>

  DataFetcher에서 onDataReady()콜백을 주면 하는 동작은 DecodeJob으로 onDataFetcherReady()콜백을 넘기는 것이다.

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
// DataCacheGenerator.java
@Override
public void onDataReady(Object data) {
  cb.onDataFetcherReady(sourceKey, data, loadData.fetcher, DataSource.DATA_DISK_CACHE, sourceKey);
}
 
 
// ResourceCacheGenerator.java
@Override
public void onDataReady(Object data) {
  cb.onDataFetcherReady(sourceKey, data, loadData.fetcher, DataSource.RESOURCE_DISK_CACHE,
      currentKey);
}
 
 
// SourceGenerator.java
@Override
public void onDataReady(Object data) {
  DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();
  if (data != null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) {
    dataToCache = data;
    // We might be being called back on someone else's thread. Before doing anything, we should
    // reschedule to get back onto Glide's thread.
    cb.reschedule();
  } else {
    cb.onDataFetcherReady(loadData.sourceKey, data, loadData.fetcher,
        loadData.fetcher.getDataSource(), originalKey);
  }
}
cs

  - DataCahceGenerator는 이미지 데이터의 원본이 캐쉬되어있는 영역에서 가져오기 때문에, attemptKey로서 sourceKey를 전달한다.

  - ResourceCacheGenerator는 사용하고자하는 view의 width, height에 맞는 리소스가 캐쉬되어있는 영역에서 가져오기 때문에 sourceKey에 width, height등 여러가지 옵션을 추가하여 currentKey를 만들고, 이것을 콜백으로 전달한다.

 - SourceGenerator는 File 또는 Network I/O로 가져오기 때문에 다른 스레드에서 호출될 수 있으므로 예외처리를 해준다.


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
// DecodeJob.java
 
@Override
public void onDataFetcherReady(Key sourceKey, Object data, DataFetcher<?> fetcher,
    DataSource dataSource, Key attemptedKey) {
  this.currentSourceKey = sourceKey;
  this.currentData = data;
  this.currentFetcher = fetcher;
  this.currentDataSource = dataSource;
  this.currentAttemptingKey = attemptedKey;
  if (Thread.currentThread() != currentThread) {
    runReason = RunReason.DECODE_DATA;
    callback.reschedule(this);
  } else {
    decodeFromRetrievedData();
  }
}
 
private void decodeFromRetrievedData() {
  if (Log.isLoggable(TAG, Log.VERBOSE)) {
    logWithTimeAndKey("Retrieved data", startFetchTime,
        "data: " + currentData
        + ", cache key: " + currentSourceKey
        + ", fetcher: " + currentFetcher);
  }
  Resource<R> resource = null;
  try {
    resource = decodeFromData(currentFetcher, currentData, currentDataSource);
  } catch (GlideException e) {
    e.setLoggingDetails(currentAttemptingKey, currentDataSource);
    exceptions.add(e);
  }
  if (resource != null) {
    notifyEncodeAndRelease(resource, currentDataSource);
  } else {
    runGenerators();
  }
}
cs

 - decodeFromData()를 통해 resource를 생성하는 과정은 매우 복잡한 호출구조로 이루어져있으나, 다음과 같이 Java에 기본으로 탑재된 BitmapFactory를 이용해 Decode한다.

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
// Downsampler.java
 
private static Bitmap decodeStream(InputStream is, BitmapFactory.Options options,
    DecodeCallbacks callbacks) throws IOException {
  if (options.inJustDecodeBounds) {
    is.mark(MARK_POSITION);
  } else {
    // Once we've read the image header, we no longer need to allow the buffer to expand in
    // size. To avoid unnecessary allocations reading image data, we fix the mark limit so that it
    // is no larger than our current buffer size here. We need to do so immediately before
    // decoding the full image to avoid having our mark limit overridden by other calls to
    // markand reset. See issue #225.
    callbacks.onObtainBounds();
  }
  // BitmapFactory.Options out* variables are reset by most calls to decodeStream, successful or
  // otherwise, so capture here in case we log below.
  int sourceWidth = options.outWidth;
  int sourceHeight = options.outHeight;
  String outMimeType = options.outMimeType;
  final Bitmap result;
  TransformationUtils.getBitmapDrawableLock().lock();
  try {
    result = BitmapFactory.decodeStream(is, null, options);
  } catch (IllegalArgumentException e) {
    throw newIoExceptionForInBitmapAssertion(e, sourceWidth, sourceHeight, outMimeType, options);
  } finally {
    TransformationUtils.getBitmapDrawableLock().unlock();
  }
 
  if (options.inJustDecodeBounds) {
    is.reset();
 
  }
  return result;
}
cs


 - 이렇게 Decode, Transform이 끝나면 요청에대한 complete을 리턴하고, 해당 리소스를 재인코딩하여 캐쉬에 저장한다. (DecodeJob.java 16행)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// DecodeJob.java
 
private void notifyEncodeAndRelease(Resource<R> resource, DataSource dataSource) {
  Resource<R> result = resource;
  LockedResource<R> lockedResource = null;
  if (deferredEncodeManager.hasResourceToEncode()) {
    lockedResource = LockedResource.obtain(resource);
    result = lockedResource;
  }
 
  notifyComplete(result, dataSource);
 
  stage = Stage.ENCODE;
  try {
    if (deferredEncodeManager.hasResourceToEncode()) {
      deferredEncodeManager.encode(diskCacheProvider, options);
    }
  } finally {
    if (lockedResource != null) {
      lockedResource.unlock();
    }
    onEncodeComplete();
  }
}
cs

그리고는 DecodeJob을 모두 정리한다. 

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
// DecodeJob.java
 
/**
 * Called when we've finished encoding (either because the encode process is complete, or because
 * we don't have anything to encode).
 */
private void onEncodeComplete() {
  if (releaseManager.onEncodeComplete()) {
    releaseInternal();
  }
}
 
private void releaseInternal() {
  releaseManager.reset();
  deferredEncodeManager.clear();
  decodeHelper.clear();
  isCallbackNotified = false;
  glideContext = null;
  signature = null;
  options = null;
  priority = null;
  loadKey = null;
  callback = null;
  stage = null;
  currentGenerator = null;
  currentThread = null;
  currentSourceKey = null;
  currentData = null;
  currentDataSource = null;
  currentFetcher = null;
  startFetchTime = 0L;
  isCancelled = false;
  exceptions.clear();
  pool.release(this);
}
cs


이로서 Android에서 요즘 Image관련 툴로 핫한 Glide에대한 소스레벨까지의 분석을 하였다.

이정도만 알면 OOM이나 로딩 지연에대한 디버깅을 하는데에는 문제 없을 것으로 보인다.

반응형
Comments