[Android / Glide] GlideApp 생성절차

아래 방식은 4.x.x 버전에서 유효하다. 3.x.x 버전의 Glide에서는 Manifest.xml에 meta-data를 추가하는 방식으로 진행된다.
참고) 3.x.x는 아래 링크가 더 도움이 될 것 같다. 3.x.x에서는 AppGlideModule이 아닌 GlideModule을 상속받는 클래스를 만들어야 한다.
https://medium.com/@PaulinaSadowska/adding-headers-to-image-request-in-glide-dc9640ca9b12

1. app/build.gradle에 dependencies 추가

1
2
3
4
5
6
7
8
9
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
...
dependencies {
...
    implementation 'com.github.bumptech.glide:glide:4.11.0'
    implementation "com.github.bumptech.glide:okhttp3-integration:4.11.0"
    kapt 'com.github.bumptech.glide:compiler:4.11.0'
}

코틀린을 쓰고 있다면, glide:compiler dependencies를 kapt로 추가해줘야한다.
이를 위해 선행작업으로 gradle.build의 최상단에 kotlin-kapt 도 필요하다.

  • glide:glide는 기본적으로 Glide 라이브러리를 사용하기 위해 추가됨.
  • okhttp3-integration는 CustomGlideModule에서 사용하기 위함.
  • glide:compiler는 GeneratedAppGlideModuleImpl를 생성하기 위함.
    이후 proguard-rules.pro에서 GeneratedAppGlideModuleImpl를 keep 해주는데 이는 GlideApp을 사용하기 위해 CustomGlideModule과 같은 패키지(net.common.utils.GlideApp)에 만들어진다.(4번 절차 참조)

2. UserAgentInterceptor 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.IOException;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;

public class UserAgentInterceptor implements Interceptor {

private Context mContext;
public UserAgentInterceptor(Context context) {
mContext = context;
}

@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request()
.newBuilder()
.header("User-Agent", getUserAgent(mContext))
.build();
return chain.proceed(request);
}
}

3. CustomGlideModule 생성

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
package net.common.utils;

import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.Registry;
import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.module.AppGlideModule;
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader.Factory;
import java.io.InputStream;
import okhttp3.OkHttpClient;

@GlideModule
public final class CustomGlideModule extends AppGlideModule {

@Override
public void applyOptions(@NonNull Context context,
    @NonNull GlideBuilder builder) {
// 아무 것도 수정하지 않지만 오버라이드 해줘야함
super.applyOptions(context, builder);
}

@Override
public void registerComponents(@NonNull Context context,
    @NonNull Glide glide, @NonNull Registry registry) {
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new UserAgentInterceptor(context))
.build();
registry.replace(GlideUrl.class, InputStream.class, new Factory(client));
}
}
  • @GlideModule 어노테이션 선언 꼭 잊지 말 것.
  • 3.x.x와는 다르게 AppGlideModule을 상속받아 커스텀 클래스를 만들어준다.
  • registerComponents에서 OkHttpClient를 통해 커스텀 인터셉터를 추가해준다.

AppGlideModule과 LibraryGlideModule

AppGlideModule(을 상속받은 CustomAppGM)은 애플리케이션 단에, LibraryGlideModule(을 상속받은 ~ 생략)은 공통 라이브러리 단에 구현하면 된다.

이때 GlideModule을 커스텀할 경우, CustomAppGM의 구현은 필수적이나 CustomLibraryGM의 구현은 선택적이어서, CustomLibraryGM만 구현해선 안 된다.

그리고 공통 라이브러리에 정의한 모든 CustomLibraryGM은 CustomAppGM에서 통합적으로 추가된다.

4. 재빌드

3번까지 진행 후 재빌드(Build>Make Project) 하자.

  • app/build/generated/source/kapt/appDebug/com.bumptech.glide.GeneratedAppGlideModuleImpl
  • app/build/generated/source/kapt/appDebug/net.common.utils.GlideApp

위와 같은 로케이션에 GeneratedAppGlideModuleImplGlideApp이 생성되는 것을 확인할 수 있다. 이렇게 되면 이제 GlideApp을 사용할 준비가 된 것.

  • GlideApp은 CustomGlideModule을 정의한 패키지 내에 만들어진다.

  • 위의 두 파일이 만약 생성되지 않는다면

    • build.gradle의 kapt를 확인할 것
    • @GlideModule 어노테이션 넣었는지 확인할 것

5. app/proguard-rules.pro 편집

1
2
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep class com.bumptech.glide.GeneratedAppGlideModuleImpl

위와 같은 keep을 추가해줘야한다.

첫번째 라인(* extends AppGlideModule)은 CustomAppGlideModule을 위해서 쓰임.

두번째 라인(GeneratedAppGlideModuleImpl)은 빨간줄로 오류표시가 날 수 있는데, 4번에서 말한 것처럼 kapt로 추가된 의존성에 의해 빌드 후 생성되는 파일이라서이다. 빌드 후 generated에 생성되는 게 GeneratedAppGM과 GlideApp인 것으로 보아 GlideApp을 사용하기 위해서 쓰이는 걸로 보인다.

(참고로 app/proguard-rules.pro 파일에서 #으로 시작되는 라인은 주석에 해당함.)

6. GlideApp 사용예

1
2
3
4
GlideApp.with(context)
.load("url")
.apply(options)
.into(imageView);

GlideApp을 통해 이미지를 로드해야지만이 CustomGlideModule에서 정의한 설정을 따른다.

기본적으로 Glide와 사용에 지장이 있을 정도로 방식이 다르지 않다.

단, GlideApp은 Glide와는 다르게 일부 RequestOptions를 통해서만 호출할 수 있었던 속성들을 다이렉트로 컨트롤할 수 있게 된다. 이는 공식문서에서 일부 예제들을 통해 비교할 수 있다.

[Android / Glide] Glide v4 적용

1. 시작하기

안드로이드 SDK 요구사항

  • 최소 SDK 버전 - Ice Cream Sandwich, 14 이상
  • 컴파일 SDK 버전 - Oreo MR1, 27 이상

권한

1
2
3
4
5
<uses-permission android:name="android.permission.INTERNET" />
// Glide가 연결 상태를 감시하고 실패한 요청을 재시작하는 것을 허용
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
// ExternalPreferredCacheDiskCacheFactory를 사용하여 Glide의 캐시를 공개 sdcard에 저장하기 위해
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

의존성

1
2
3
dependencies {
kapt 'com.github.bumptech.glide:glide:4.11.0' // 코틀린을 쓸 경우 kapt여야함.
}

2. 기본 형태

1
2
3
4
5
6
7
8
9
RequestOptions options = new RequestOptions()
.bitmapTransform(new RoundedCorners(20)) //radius
.placeholder(R.drawable.loading) // 이미지 로딩 중
.error(R.drawable.loading); // 이미지를 불러오지 못할 경우

Glide.with(context) // RequestManager 형태로 반환
.load("url"// RequestBuilder<Drawable> 형태로 반환 (이하 apply, into 동일하게 반환)
.apply(options)
.into(imageView);

ImageView의 setDrawableResource(resource)를 부르는 것으로 보임.

  • apply에 if문을 적용한 사례
    1
    2
    3
    4
    5
    6
    7
    8
    Glide.with(this)
    .load(url)
    .transition(DrawableTransitionOptions.withCrossFade(factory))
    .diskCacheStrategy(DiskCacheStrategy.ALL)
    .apply {
    if (corner = 0) transfoem(CenterInside(), RoundedCorners(corner.fromDpToPx()))
    }
    .into(this)

3. Glide 커스텀을 위한(=GlideApp 사용을 위한) AppGlideModule

Header에 User-Agent property 삽입

GlideApp 생성절차 링크를 따를 것.

4. 비트맵 관련 설정 - RequestOptions

(1) 주요 옵션

  • placeholder(resourceId): 이미지 로딩하는 중에 보여지는 대체 이미지.
  • error(resourceId): 이미지를 불러오지 못할 경우 대체 이미지.
  • skipMemoryCache(boolean): true 설정 시 캐시를 통하지 않고 이미지 로드.
  • override(int width, int height): target의 너비, 높이를 세팅. 이미지 리사이징.
  • centerCrop: default type. 외에 FitCenter, CircleCrop도 있음.

Glide에서 RequestOptions를 Glide.apply()했을 때와 안 했을 때의 이미지 transformation default 값이 다르다. (아래 예시: v4 공식문서 발췌)

→ RequestOptions을 적용하지 않았을 경우 default 값
Glide.with(context)
.load(url)
.fitCenter() // default
.into(imageView);

→ RequestOptions을 적용 시 default 값
Glide.with(context)
.load(url)
.apply(new RequestOptions().centerCrop())
.into(imageView);

→ xml에서 scaleType지정하면 그걸로 적용될 것 같은데, 만약 ImageView인데 scaleType을 지정하지 않았을 경우 AUIL와 Glide(RequestOptions적용한 것), Glide(RO 적용 안 한 것) 세 개의 작동이 다를 수 있음.

(2) 참고 옵션

  • diskCacheStrategy: 디스크 캐시 전략. 이미지 로드에 사용할 캐시 설정 ALL - DATA와 RESOURCE를 사용하여 원격 데이터를 캐시하고 RESOURCE만 사용하여 로컬 데이터를 캐싱 AUTO - defualt value. NONE - 디스크캐시 사용안함.
  • priority: 로드되는 이미지의 우선순위 결정 HIGH, IMMEDIATE, LOW, NORMAL
  • fallback: load할 url이 null일 경우 보여줄 대체 이미지. 이게 정의되어있지 않으면 error 이미지를, error 이미지도 없다면 placeholder 이미지로 대체됨.

(3) 예제. DisplayImageOptions와 대응되는 RequestOptions

1
2
3
4
5
6
7
8
9
private DisplayImageOptions mPlaceHolderOption = new DisplayImageOptions.Builder()
.resetViewBeforeLoading(true) // default
.cacheInMemory(true) // default
.cacheOnDisk(true) // default
.imageScaleType(ImageScaleType.EXACTLY) // default
.bitmapConfig(Bitmap.Config.RGB_565) // default
.showImageOnLoading(R.drawable.ic_loading) // loading place holder resource
.showImageOnFail(R.drawable.ic_loading) // failed place holder resource
.build();
  • resetViewBeforeLoading(true)
  • cacheInMemory(true) → RequestOptions.skipMemoryCache(false)
  • cacheOnDisk → RequestOptions.diskCacheStrategy
  • imageScaleType.EXACTLY: 뷰 사이즈에 맞춰서 이미지가 작아짐.
  • bitmapConfig → RequestOptions.format(DecodeFormat.PREFER_RGB_565)
      RequestOptions.format(DecodeFormat.PREFER_ARGB_8888)
    
    Glide’s default: RGB_565
  • showImageOnLoading / showImageOnFail → RequestOptions.placeholder(drawable) / RequestOptions.error(drawable)

5. Clear

(1) memory 캐시 삭제

1
2
// 이 메소드는 메인 스레드에서 호출되어야 한다.
Glide.get(context).clearMemory();

Glide의 캐시 메모리 영역과 BitmapPool을 정리한다.

단, 모든 메모리를 삭제하는 것은 특히 효율적이지 않으며 버벅거림과 로드 시간 증가를 방지하기 위해 가능한 한 피해야 한다.

(2) disk 캐시 삭제

1
2
3
4
5
6
7
8
val isMainThread = Looper.myLooper() == Looper.getMainLooper()
just<String>("")
  .subscribeOn(if (isMainThread) Schedulers.io() else Schedulers.immediate())
  .subscribe {
    // 이는 background 스레드에서 실행되어야 한다.
    Glide.get(this@SomethingActivity).clearDiskCache()
    Log.d("Deleted image memory cache and disk cache.")
}

디스크 캐시의 모든 항목을 지운다.

앱에서 실제로 테스트 해보니 캐시메모리의 디폴트 사이즈인 250MB를 채운 후 clearDiskCache()를 호출했을 때 변화는 다음과 같았다.

(Glide 외의 다른 캐시 데이터로 인해 왼쪽 before 사진에서는 250MB보다 약간 오버된 상태이다.)

6. 전환 - Transitions

1
2
3
4
5
6
import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;

Glide.with(context)
.load(url)
.transition(withCrossFade()) // optional
.into(view);

Glide v4 does NOT apply a cross fade or any other transition by default.

Glide v4 부터는 어떤 transitions 이벤트도 기본값으로 들어가있지 않으므로 필요할 떄 추가할 수 있다. (v3에서는 기본값이 cross fade였음)

7. 리스너 - RequestListener

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
Glide.with(context)
.load(url)
.listener(new RequestListener<Drawable>() { // load() 다음에 추가할 것
        @Override
        boolean onLoadFailed(@Nullable GlideException e, Object model,
        Target<Drawable> target, boolean isFirstResource) {
        // 여기서 GlideException 로그 출력
        Log.e(TAG, "Load failed: " + e.printBlrBlr());
        return false; // Target에서 onLoadFailed가 호출되는 것을 허용한다.
        }
       
        /*
          resource: 로드된 이미지.
          model: 이미지를 로드하는 데 사용된 모델.
          dataSource: 이미지 출처. DATA_DISK_CACHE, LOCAL, MEMORY_CACHE, REMOTE와 같은 값.
        */
        @Override
        boolean onResourceReady(
        Drawable resource,
        Object model,
        Target<Drawable> target,
        DataSource dataSource,
        boolean isFirstResource) {
        // 성공 로그를 출력하거나 DataSource를 사용해 캐시적중을 추적할 수 있다.
        return false; // Target에서 onResourceReady가 호출되는 것을 허용한다.
        }
    })
    .into(imageView);

만약 Bitmap으로 로딩된 이미지를 얻고자 한다면,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Bitmap loadedImage = Glide.with(context)
.asBitmap()
.load(url)
.listener(new RequestListener<Bitmap>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object o,
Target<Bitmap> target, boolean b) {
return false;
}

@Override
public boolean onResourceReady(**Bitmap bitmap**, Object o,
Target<Bitmap> target, DataSource dataSource, boolean b) {
image.setImage(ImageSource.bitmap(bitmap));
return false;
}
})
.submit(100100) // width, height: RequestBuilder API 참조
.get();

관련링크

[Android] Glide에서 비트맵 이미지가 GC되는 과정

개요

위의 공식 문서에서 나와있는 바와 같이 기본적으로 Glide로 비트맵을 관리하는 걸 추천하고 있다.

이를 관리해줘야 하는 이유는 다음과 같다.

앱에서 대량의 비트맵 데이터를 표시하면 OutOfMemoryError 오류가 발생할 수 있습니다.

OutOfMemory 오류를 방지하기 위하여 안드로이드에서는 recycle() 기능을 제공하는데, 이 메소드를 실행하면 비트맵에 사용한 메모리를 회수할 수 있다. 즉, 이 메소드를 호출함으로써 해당 비트맵은 GC의 대상이 된다. 당연히 해당 비트맵을 더이상 사용하지 않을 거라고 확정된 시점에 호출해야한다.

Glide는 비트맵의 메모리를 어떻게 관리하고 있는가?

메모리 캐시 vs 디스크 캐시 의 내용에 따르면 비트맵 캐싱은 LruCache를 사용하는 대표적인 예다. 여기서 힌트를 얻어서 Glide를 적용하면서 설정했던 BitmapPool 사이즈 디폴트값을 보자. 관련 코드는 다음과 같다.

1
2
3
// 비트맵 풀: The default size is based on the screen size and density of the device
int bitmapPoolSizeBytes = 1024 * 1024 * 30; // 30mb
builder.setBitmapPool(new LruBitmapPool(bitmapPoolSizeBytes));

BitmapPool 사이즈는 단말의 스크린사이즈와 density 값에 따라 달라진다고 나와있다.

Glide에서 비트맵의 라이프사이클 링크

Glide에서 비트맵을 관리하는 데에 있어 일반적인 생명주기는 다음과 같다.

InputStream -> Bitmap -> View -> Memory Cache -> Bitmap Pool -> Recycle

해당 링크 해석본

비트맵은 메모리 캐시 또는 BitmapPool에서 View(또는 target) 및 Loop로 이동할 수도 있습니다. 디코딩 파이프 라인 (예 : 변환을 통해) 중에 리소스를 수동으로 재활용하는 경우 비트맵이 표시되지 않고 recycled state가 될 수도 있습니다. 그렇지 않으면 비트맵이 크기 제약으로 인해 제거될 때 메모리 캐시에서 BitmapPool로, BitmapPool에서 recycled state로 이동합니다.

“Cannot obtain size for recycled Bitmap” 오류는 Recycle state에 도달한 비트맵을 그리려고 할 때 발생합니다. 결코 발생해서는 안되지만 다음과 같은 몇 가지 시나리오에서 발생할 수 있습니다.

  1. 변환 중에 디코딩 프로세스 중에 비트맵을 수동으로 recycle할 경우

    이 원인은 일반적이지 않은 브랜치에서 커스텀 변환을 하지 않는 한 이런 상황을 꽤 자주 볼 수 있습니다.

  2. 디코딩 프로세스 중, 변환 중에 두 번 이상 Pool에 비트맵을 반환한 경우

    이 경우 문제의 원인을 파악하기 쉽습니다. 비트맵이 Pool에서 제거되지 않고 Pool에 여러 번 추가되면 다른 이미지가 동시에 표시되어, 두 개의 이미지 중 하나는 올바르게 표시되고 다른 하나는 잘못 표시 될 수 있습니다. 게다가 다른 뷰에서 참조하는 동안 동시에 recycle 될 수도 있습니다.

  3. onLoadCleared가 호출된 후에도 계속 리소스를 참조하거나, 또는 해당 Target이 지워진 후 Target에 로드된 리소스를 참조한 경우

    커스텀 target을 사용하는 경우 발생할 수 있습니다.

안타깝게도 비트맵을 추적하기 위해 수동 참조 계산을 수행해야하므로 이러한 종류의 버그를 추적하는 것이 매우 어려울 수 있습니다.
BitmapPool의 크기를 크게 줄이거나 BitmapPoolAdapter를 사용하여 충돌이 더 자주 발생하는지 확인하십시오. 메모리 캐시의 크기를 줄일 수도 있습니다. 두 경우 모두 파이프 라인의 길이를 줄입니다. 이는 Bitmap이 Recycled state에 더 빨리 도달하여 오류가 더 자주 발생할 수 있음을 의미합니다. 또한 비트맵 렌더링 오류, 기록 된 GL 경고 또는 다른 이미지 대신 잘못된 이미지가 나타나는지 주시하십시오. 이 모든 것은 버그를 재현한 신호입니다.

결론

따라서 Glide는 변환된 Bitmap을 내부적으로 recycle하기 때문에 imageView.setRecycler()하지 않아도 된다.

[Android] 웹뷰에서 동작하는 페이스북 로그인이 deprecated됨

1. 공지 전문

(1) 관련 문의 및 공식답변 링크

안녕하세요 철구 님, 자세한 내용에 감사드립니다.
로그 파일에서 ‘PLATFORM__LOGIN_DISABLED_FROM_WEBVIEW_OLD_SDK_VERSION’ 오류 코드를 확인할 수 있습니다. Android 웹뷰를 통한 Facebook 로그인(https://developers.facebook.com/blog/post/2021/06/28/deprecating-support-fb-login-authentication-android-embedded-browsers/)이 중단되었기 때문에 발생한 것으로 보입니다.
대신 이 링크를 통해 사용자 지정 탭을 적용하는 자세한 지침을 확인할 수 있습니다.
만약 문제가 지속된다면 사용 중인 SDK 버전과 영어로 표시된 오류 메시지 스크린숏과 함께 오류를 재현할 수 있도록 샘플 프로젝트를 제공 부탁드립니다.
감사합니다.

2. 공식 해결 가이드

(1) 페이스북 SDK 버전이 8.2 이상인지 확인

1
implementation 'com.facebook.android:facebook-android=sdk:8.2.0'

(2) 수정사항

앱 내부 웹뷰가 아닌 외부 브라우저를 통해 페이스북 로그인 진행하도록 변경

2021년 10월 5일부터 Facebook 로그인은 사용자 로그인에 Android 내장 브라우저(WebView) 사용을 더 이상 지원하지 않습니다. 사용자 경험이 중단되지 않도록 하려면 다음 체크리스트를 사용하여 대신 사용자 지정 탭을 사용하십시오.

3. 결론

웹뷰를 통해 페이스북 로그인 페이지에 접근하면 안 되며, 외부 브라우저를 띄워 페이스북 로그인 페이지에 접근하도록 수정해야 한다.

[Android] JCenter 지원종료

현재 상황(2021년 후반)

[Gradle] JCenter shutdown impact on Gradle builds에 따르면 JFrog에서 아래와 같이 공지했다고 한다.

  • JCenter는 read-only로 계속 제공될 것이나
  • 더이상 새로운 버전의 패키지는 받을 수 없다.
  • 모든 Bintray 서비스(JCenter에 배포하기 위한 플랫폼)는 중지되었다.

따라서 JCenter에서 기존에 있던 외부 라이브러리에 대한 정보는 계속 제공되기 때문에 현재 상황에서 바로 대응할 부분은 없을 것이다.

다만, 안드로이드 앱의 경우 build.gradle에 의존성을 추가한 외부 라이브러리의 버전 업데이트를 할 때 그 상위 버전이 JCenter에 업데이트 되어있지 않을테니 그때 해당 라이브러리 버전에 맞는 classpath를 추가하면 될 것으로 보인다.


JCenter 지원중단 시 상황(2021년 초중반)

JFrog에서 운영하는 JCenter, GoCenter 및 ChartCenter가 운영을 중지할 예정이다.

2021년 2월 28일 : GoCenter, ChartCenter에 라이브러리 등록 서비스 중지

2021년 3월 31일 : JCenter에 라이브러리 등록 서비스 중지

2021년 5월 1일 : GoCenter, ChartCenter에서 라이브러리 다운로드 서비스 중지

2022년 2월 1일 : JCenter에서 라이브러리 다운로드 서비스 중지

이유는 위의 요약링크에서 토론에서 말하기를

ExoPlayer의 issue에서 언급되기를, JCenter에서 악성코드가 포함된 라이브러리 패키지가 등록된 사고가 있었다고 하며, 이러한 사고방지 대책을 세우지 못하고 서비스를 닫는 것으로 보인다.

안드로이드 라이브러리는 JCenter에 의존한 게 많은데 따라서 대책을 세워야 한다. JCenter는 아래와 같이 레포지토리에 등록되어있다.

1
2
3
4
5
6
repositories {
google()
mavenCentral()
jcenter()
maven { url 'https://maven.fabric.io/public' }
}

또한 이 JCenter 지원 종료에 대한 안드로이드 공식 입장은 다음과 같다.

2022년 2월 1일까지 JCenter에서 기존 아티팩트를 다운로드할 수 있습니다. - 공식 문서

이에 관하여 추후에 또다른 공지가 올라올 예정이다.