[Android] 메모리 캐시와 디스크 캐시

브라우저에서의 메모리/디스크 캐시

메모리 캐시 vs 디스크 캐시 :: 마이구미 (정리잘되어있음)

아래는 해당 링크의 요약본이다.

브라우저 측면이 아닌 하드웨어 측면에서 보면,

  • 관련설명 잘 되어있는 추천영상: [bRd 3D] CPU는 어떻게 작동할까? 유투브링크
  • 디스크 캐시는 하드디스크에 접근하는 시간을 개선하기 위해 RAM에 저장하는 기법이다.
  • 캐시 메모리는 램에 접근하지 않고 더 빠른 시간으로 접근할 수 있는 CPU 칩 안에 있는 작지만 빠른 메모리이다.(L1, L2, L3)

브라우저 측면에서 보면,

기본적으로 캐시 데이터는 하드디스크에 저장되는데(즉, 디스크 캐시), 재사용 및 I/O 최소화 등의 이점이 있으나 더 빠른 처리를 위해 I/O 없이 웹코어 내부로 바로 접근하여 데이터를 가져오는 메모리 캐시가 존재한다.

앱, 비트맵 캐싱

비트맵 캐싱은 LruCache를 사용하는 대표적인 예다.

디스크 캐시 및 메모리 캐시에 대해 구분하고자 한 계기는 Glide다. Glide에서는 비트맵 풀 설정을 재정의할 때 다음과 같이 사용한다.

1
2
int bitmapPoolSizeBytes = 1024 * 1024 * 30; // 30mb
builder.setBitmapPool(new LruBitmapPool(bitmapPoolSizeBytes));

(참고로 기존에 사용하던 UIL의 설정 중 하나는 .memoryCache(new LruMemoryCache(2 * 1024 * 1024))였다.)

또한, 메모리 캐시를 사용하지 않거나 디스크 캐시를 어떻게 사용할지 Strategy(전략)을 설정할 수 있도록 하였다.

1
2
.skipMemoryCache(true) // default is false
.diskCacheStrategy(DiskCacheStrategy.NONE) // default is AUTO

그렇다면 왜 이 둘은 나뉘어져 있고, 왜 Lru를 사용할까.

비트맵 캐싱 측면에서 보는 메모리 캐시와 디스크 캐시

메모리 캐시는 “중요한 애플리케이션 메모리”를 사용하는 대신 비트맵에 빠르게 액세스할 수 있다. (인용 출처: 공식문서)

주석 > 링크 “중요한 애플리케이션 메모리”는 View > Tool Windows > Device File Explorer 로 들어가서 data/data 하위 폴더에서 확인할 수 있다.

하지만 애플리케이션이 종료되면 메모리 캐시는 사라지기 때문에, 일부 데이터는 “유지”될 필요가 있다.

이러한 경우 디스크 캐시를 사용하여 처리된 비트맵을 “유지”하고 메모리 캐시에서 이미지가 더 이상 사용 가능하지 않을 때 로드 시간을 줄일 수 있습니다.

결론

즉, 메모리 캐시는 빠르다. 앱 자체에서 가장 빠르게 도달할 수 있는 메모리에 저장되기 때문이다. 하지만 그 용량이 작아 이미지 로딩이 많은 경우 문제가 될 수 있으므로 디스크 캐시를 함께 사용하게된다.

디스크 캐시는 메모리 캐시에 비해서는 느리다. 따라서 여기에서 이미지를 불러올 경우 백그라운드에서 로딩이 되도록 비동기 처리를 함께 해줘야하는 대신 용량이 크다.

[Android / Glide] 이미지 URL을 읽어 JPG 포맷파일로 변환하기

1. remote url의 이미지를 비트맵 형식으로 가져온다.

1
2
3
4
5
6
7
fun loadBitmapImage(context: Context, url: String?, listener: RequestListener<Bitmap?>?) {
GlideApp.with(context)
.asBitmap()
.load(url)
.listener(listener)
.preload() //submit()으로 하면 Activity destroy 시점에서 오류
}

Glide를 사용해 listener를 통해 onResourceReady에서 비트맵 리소스를 받아 처리한다.

Glide를 쓰지 않고 가령, 갤러리에 저장된 이미지를 가져와 3번으로 넘어갈 경우, imageView에 비트맵을 저장하게 되는데, 이때 Bitmap의 recycle state를 관리해줘야할 수 있다. (참고: Glide에서 비트맵 이미지가 GC되는 과정)
이런 이유로 Android Developer 공식 문서에서는 대부분의 경우 Glide 라이브러리를 사용하여 앱에서 비트맵을 가져오고 디코딩하고 표시하는 것을 추천한다고 나와있다.

2. 비트맵을 JPG 포맷으로 압축, 저장한다.

이제 받아온 비트맵을 remoteTemp.jpg 라는 이름의 파일에 JPG 포맷으로 저장할 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mRemoteImageUri = Uri.fromFile(getReviewTempImageFile(context
, "remoteTemp.jpg"));
File bitmapFile = new File(mRemoteImageUri.getPath());

try {
FileOutputStream outputStream = new FileOutputStream(bitmapFile);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);

outputStream.flush();
outputStream.close();

setPickedImage(mRemoteImageUri);
} catch (Exception e) {
e.printStackTrace();
}

먼저 저장할 파일의 Uri를 만들었다. Uri를 생성하는 이유는 3번에서 파일의 사이즈를 얻어오기 위함이다. 자세히는 해당 번호 참조.

compress의 두 번째 인자값은 CompressFormat에 대한 압축률이며 0(minimum) ~ 100(maximum)으로 설정할 수 있다.

bitmap.compress(CompressFormat.PNG, 0, filestream)
위와 같이 PNG로 압축하여 출력할 경우 두 번째 인자값은 무시한다. PNG 파일의 특성이 손실률 없는 포맷에 해당하기 때문이다.

위의 코드에서는 FileOutputStream 객체를 사용하여 JPG로 압축한 비트맵이 파일에 저장되었으나 이를 파일로 저장하지 않고 처리하려면 ByteArrayOutputStream를 사용할 수 있다.

  • 코드 세부사항 파일에 저장하는 코드 상에서 사용한 getReviewTempImageFile 메소드는 아래와 같다. 여기서 ‣ 주의할 것.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public static File getReviewTempImageFile(Context context, String imageName) {
    String imgDirName = "/TestDir";
    String dirPath = context.getExternalFilesDir(null) + imgDirName;
    File saveDir = new File(dirPath);
    if (!saveDir.exists()) {
    saveDir.mkdir();
    }

    String nomediaFilePath = dirPath + "/.nomedia";
    File nomediaFile = new File(nomediaFilePath);
    if (!nomediaFile.exists()) {
    try {
    nomediaFile.createNewFile();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }

    String filePath = dirPath + "/" + imageName;
    return new File(filePath);
    }
    getExternalFilesDir()을 사용하기 때문에 미리 WRITE_EXTERNAL_STORAGE 퍼미션 처리를 해주어야 한다. crop을 수행하는 Activity에 진입하기 전에 체크해주는 게 가장 좋다. 해당 퍼미션을 체크하는 코드는 다음과 같다.
    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
    private void doImageSearch() {
        if (permissionCheck(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
            // start CropActivity
    } else {
    mRequestPermission = Manifest.permission.WRITE_EXTERNAL_STORAGE;
    requestPermission(this, PermissionConstants.REQUEST_STORAGE_CODE, Manifest.permission.WRITE_EXTERNAL_STORAGE);
    }
    }

    public static boolean permissionCheck(Context context, String permission) {
        return Build.VERSION.SDK_INT < Build.VERSION_CODES.M
            || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
            && ActivityCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED);
    }

    public static void requestPermission(Context context, int requestCode, String permission) {
        //shouldShowRequestPermissionRationale - 사용자가 권한 요청을 한번 거절 했을 경우 True
        if (ActivityCompat.shouldShowRequestPermissionRationale((Activity) context, permission)) {
            try {
    Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
    .setData(Uri.parse("package:" + context.getPackageName()));
    ((Activity) context).startActivityForResult(intent, PermissionConstants.REQ_CODE_REQUEST_SETTING);
    } catch (ActivityNotFoundException e) {}
        } else {
           // 처음 권한 요청 및 다시 보지 않기를 선택하였을 경우 false
            ActivityCompat.requestPermissions((Activity) context, new String[]{permission}, requestCode);
        }
    }

3. 이미지 파일을 적절한 사이즈로 변경하여 재저장

crop을 준비하기 위해서는 디바이스의 한 화면에 적절한 크기로 이미지를 노출해야한다. 위의 코드에서 setPickedImage 메소드가 이 기능을 담당한다.

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
49
private void setPickedImage(@Nullable Uri rawBitmap) {
    mImageCaptureUri = Uri.fromFile(getReviewTempImageFile(context, "originTemp.jpg"));
    File bitmapFile = new File(mImageCaptureUri.getPath());
    try {
        Uri bitmapUri = mImageCaptureUri;
        if(rawBitmap != null)
            bitmapUri = rawBitmap;

        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inSampleSize = calculateBitmapSampleSize(this, bitmapUri);
        options.inJustDecodeBounds = false;

        InputStream is = getContentResolver().openInputStream(bitmapUri);
        Bitmap bitmap = BitmapFactory.decodeStream(is, null, options);

        if (bitmap == null) return;

        String uriPath = getRealPathFromURI(context, bitmapUri);
        if(TextUtils.isEmpty(uriPath))
            uriPath = bitmapUri.getPath();
        ExifInterface exif = new ExifInterface(uriPath);

        int exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
        int exifDegree = exifOrientationToDegrees(exifOrientation);
        bitmap = rotate(bitmap, exifDegree);
        FileOutputStream outputStream = new FileOutputStream(bitmapFile);
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);

        outputStream.flush();
        outputStream.close();

        startCrop(bitmap);
    } catch (Exception e) {
        e.printStackTrace();
        finish();
    }
}

@Override
public void finish() {
    if(mImageCaptureUri != null)
        deleteTempImageFile(mImageCaptureUri);
    if(mRemoteImageUri != null)
        deleteTempImageFile(mRemoteImageUri);
    mCropImage.setImageBitmap(null); // clear bitmap in imageView

    super.finish();
    overridePendingTransition(0,0);
}

[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();

관련링크

기술 블로그 - 구글 검색엔진 최적화(SEO;Search Engine Optimization) 적용

  • 이카루스에서 세팅하는 방법 - 링크

구 이카루스에서 ejs를 사용하다가 jsx로 바뀌어 위의 방법이 통하지 않았음.(링크)

링크에 따르면 구버전에 비해 React를 사용해 만든 기존 이카루스에서는 표준링크를 자동으로 생성하기 때문에 hexo-auto-canonical를 따로 설치하지 않아도 된다.(그런데 설치해버려서 npm uninstall 로 삭제.)

1
2
3
4
npm i hexo-autonofollow
npm i hexo-generator-feed
npm i hexo-generator-seo-friendly-sitemap
npm i hexo-generator-robotstxt

결국 설치하는 건 위의 4개.

[1] 검색 엔진 등록: 구글 서치 콘솔

이 단락에서는 [사이트맵 생성]과 [색인 생성]방법을 알아볼 건데, 우선 규모가 작은 웹사이트의 경우 아래와 같이 구글서치콘솔 가이드에서 사이트맵이 필요하지 않을 수도 있다고 나와있다.

나는 아래에도 나와있지만 사이트맵 생성에 오류가 있어 일단 사이트맵 대신 페이지마다 [색인 생성]을 하는 방법으로 각 페이지를 구글검색에 걸리도록 하였다. (현재 페이지가 4개 정도밖에 없음)

그렇다고 사이트맵을 생성하지 말아야하는 것은 아니다.

(1) 사이트맵 생성: 아직도 안됨

Google Search Console에서 URL 접두어를 이용하여 도메인을 등록한다.

[계속]을 눌렀을 때 나오는 파일을 다운로드하여 [프로젝트]/public/ 에 해당 파일을 옮겨 넣는다. 그리고 배포 후 조금 기다리면 아래와 같이 초록색이 뜬다.

소유권이 확인되면 [속성으로 이동] 또는 왼쪽 위 URL 드롭다운을 클릭하여 Search Console을 이용할 수 있다.

지금은 검색 엔진을 사용하기 위해 사이트맵을 rss2.xml과 sitemap.xml을 등록해준다.

  • “가져올 수 없음”에 관한 포스팅

그런데 “가져올 수 없음”이라고 실패/성공도 아닌 것이 뜬다. 사이트맵 입력 시 슬래시를 붙여서 “/sitemap.xml”으로 하면 잘 된다는 말이 있어서 해보았으나 다르지 않음. 아마 현재 설연휴라서 이게 처리가 늦어지는 것으로 보고 일단 보류하고 다른 방법을 찾아보았다.

(2) 색인 생성: 1-2일 소요

주의 > 색인 생성은 일일 할당량(10개 아래, 정확하지 않음)이 있어 이를 초과하면 더이상 색인 생성을 요청할 수 없다.

색인 생성 방법은 다음과 같다.

  • 참고: 구글 서치 콘솔에서 색인 생성하는 방법 링크

상단 URL 검사란에 포스팅 주소(https://dl137584.github.io/2022/01/31/JCenter-지원종료/)를 붙여넣고 아래 [색인 생성 요청]을 누른다.

요청이 완료되면 아래와 같은 문구가 뜬다.

기본적으로 1-2일 소요되긴 하나, 1. 구글검색창에 나와도 아래 [색인생성범위]에는 뜨지 않거나 2. 이틀이 지나도 구글검색창이나 [색인생성범위]에 리스팅되지 않아서 다시 URL 검사를 해보면 등록되어있지 않다고 나오는 등의 상황이 있었다.

1번의 경우 7일 이하로 기다리면 나타나거나 하는데 2번의 경우 나는 다시 색인생성을 요청하였다.(sitemap.xml이 계속 “가져올 수 없음”이 떴기 때문에 어쩔 수 없이…)

정상적으로 색인생성요청이 처리가 된 후에는 사이드바 [색인생성범위]에서 아래 [유효]를 클릭하면 초록색으로 변하면서 그래프에도 유효 카운트가 표시되고 그 아래에 상세정보가 뜬다.

유효 색인 상세정보

색인을 생성한 url을 확인할 수 있으나 여기서 삭제는 할 수 없다.(해당 페이지는 내가 md 파일명을 변경하여 주소가 변경되었는데도 그대로 남아있다. 단, 이것도 7일 이하로 기다리면 [제외됨]으로 옮겨지면서 자동으로 갱신된다. [제외됨]으로 자동으로 옮겨질 경우 검색창에서도 검색되지 않는지는 해보지 않아 알 수 없음.)

(3) 색인 생성된 url 삭제: 6시간 내로 됨

url이 바뀌어 올렸던 것을 삭제해야 한다면 사이드바의 [삭제]로 이동한다.

그리고[임시 삭제 항목]>[새 요청]에서 삭제하고자하는 url을 기입하여 요청할 수 있다.

1

2

3

요청 처리가 완료되어 삭제되면 더이상 구글 검색창에서 관련 키워드를 입력해도 검색되지 않는다.