[Android] Parcelable: Java와 Kotlin 구현 차이

Parcelable을 상속받아 클래스 생성할 경우 CREATOR의 정의

Parcelable protocol requires a Parcelable.Creator object called CREATOR

1
2
3
4
5
6
7
8
9
10
11
public static final Creator<DataClass> CREATOR = new Creator<DataClass>() {
@Override
public SearchInfo createFromParcel(Parcel parcel) {
return new DataClass(parcel);
}

@Override
public DataClass[] newArray(int i) {
return new DataClass[i];
}
};

이걸 코틀린 코드로 변환하면 다음과 같이 만들 수 있으나-

1
2
3
4
5
6
7
8
9
10
@JvmField // 프로젝트에 자바 클래스도 존재하는 경우 JvmField 어노테이션 누락에 주의할 것 
val CREATOR: Parcelable.Creator<DataClass> = object : Parcelable.Creator<DataClass> {
override fun createFromParcel(parcel: Parcel): DataClass {
return DataClass(parcel)
}

override fun newArray(i: Int): Array<DataClass?> {
return arrayOfNulls<DataClass?>(i)
}
}

[코틀린] Parcelable 구현

-그러나 코틀린에서는 Parcelable 구현을 위해 @Parcelize 어노테이션을 제공하고 있기 때문에 꼭 필요한 경우가 아니면 자바와 같이 CREATOR를 만들 필요는 없다.

코틀린에서 구현한 Parcelable을 상속받은 Data Class는 다음과 같다. 이 코드는 이 자체만으로도 내부에서 CREATOR 기능을 수행한다.

1
2
3
4
5
6
7
8
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.android.parcel.Parcelize

@Parcelize
data class ParcelData(@SerializedName("A") var a: String? = "",
@SerializedName("B") var b: String? = "",
@SerializedName("C") var c: ArrayList<AnyData>? = arrayListOf()): Parcelable

참고를 위해 Parcelize 어노테이션에 작성된 주석의 일부를 가져왔다.

Instructs the Kotlin compiler to generate writeToParcel(), describeContents()[android.os.Parcelable] methods, as well as a CREATOR factory class automatically.

이는 자바 코드(또는 코틀린 코드) 상에서 putExtra를 통해 전달할 수 있다.

1
2
ParcelData p = new ParcelData();
intent.putExtra("extra", p);

그리고 intent를 통해 이동한 액티비티에서는 getParcelableExtra로 데이터를 받아올 수 있다.

[Android] 다른 앱 위에 그리기 권한: TYPE_APPLICATION_OVERLAY

[다른 앱 위의 그리기]는 폰 설정 > 애플리케이션 > 앱 > 고급 [다른 앱 위에 표시되는 앱]에서 권한 설정할 수 있다.

퍼미션

[다른 앱 위에 그리기] 설정을 추가하려면 Manifest에 이를 사용하겠다고 퍼미션을 추가해야하는데,

1
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

권한: 다른 앱 위에 그리기(Draw over other apps / Appear on top)

위 퍼미션을 추가하면 앱 설정(설정>애플리케이션>해당 앱)에 [다른 앱 위에 그리기] 정보가 뜬다. (안드로이드 버전마다 언어:영어일 때의 표시나 노출 위치는 다를 수 있다.)

중요한 것은 [다른 앱 위에 그리기]의 기본값이 버전마다 다르다는 건데,

  • API level 23(Android 6.0 Marshmallow) 미만에서는 true
  • 그 이상에서는 false

앱스토어에서 설치할 경우 기본적으로 위와 같이 설정되어 유저의 단말에 설치됨을 숙지하여 기능구현 프로세스를 짜야한다.

버전 체크 방법

[다른 앱 위에 그리기] 기능이 필요할 경우 API level 23 이상에서는 버전 체크 처리를 해주어야한다.

1
2
3
4
5
6
7
/* REQ_CODE_OVERLAY_PERMISSION는 임의로 정한 상수
onActivityResult(int requestCode, int resultCode, Intent data)에서 requestCode로 받을 때 사용함 */
@TargetApi(Build.VERSION_CODES.M)
private static void onObtainingPermissionOverlayWindow(Context context) {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + context.getPackageName());
((Activity) context).startActivityForResult(intent, REQ_CODE_OVERLAY_PERMISSION);
}

덧붙여 [다른 앱 위에 그리기] 설정값이 true인지 확인하는 방법은 다음과 같다.

1
2
3
4
public static boolean alertPermissionCheck(Context context) {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& !Settings.canDrawOverlays(context);
}

주의: deprecated 된 WindowManager.LayoutParams의 플래그

  • TYPE_PHONE
  • TYPE_PRIORITY_PHONE
  • TYPE_SYSTEM_ALERT
  • TYPE_SYSTEM_ERROR
  • TYPE_SYSTEM_OVERLAY
  • TYPE_TOAST

위에 나열된 타입은 API level 26(Android 8.0 Oreo)에 deprecated 되어 아래와 같이 Android Developer 에서는 이것들 대신 TYPE_APPLICATION_OVERLAY를 쓰도록 가이드 하고 있다.

1
2
3
4
5
6
7
val param: WindowManager.LayoutParams
val flag = if (Build.VERSION.SDK_INT >= Build.Version_CODES.O) {
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
}
param = WindowManager.LayoutParams(width,
WindowManager.LayoutParams.WRAP_CONTENT,
flag, ...);

탐욕법(Greedy Algorithm)

탐욕법(Greedy Algorithm)이란?

미래를 고려하지 않고 각 단계에서 최적의 해를 찾아 모든 단계를 진행할 경우 최선의 결과에 도달한다고 생각하는 알고리즘.

특징

  • 전체적인 최적해를 보장할 수 없다.
  • 선택한 것을 번복하지 않는다.
  • 직관적

예시, 최소 신장 트리

예시, 거스름돈 최소 개수 반환

거슬러줄 돈(w)에서 동전(10, 50, 100, 500)을 뺐을 때 그 값이 가장 작은 경우의 동전을 우선 반환한다.

여기서 “뺀 값이 가장 작은 경우가 최적의 해”라는 게 이 문제에서 가장 근본적인 명제이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 거스름돈 최소 개수 반환
int change = w; //입력: 거슬러줄 돈
int n500, n100, n50, n10 = 0;
while(change >= 500) {
change -= 500; n500++;
}
while(change >= 100) {
change -= 100; n100++;
}
while(change >= 50) {
change -= 50; n50++;
}
while(change >= 10) {
change -= 10; n10++;
}
return n500 + n100 + n50 + n10;

단, 200원에 대해 동전이 위의 네 가지 밖에 없다면 최종해는 100*2로 “2”겠지만, 160원짜리 동전이 만들어진다면  160*1 + 10*4로 “5”가 나오기 때문에 탐욕법으로 최적해를 찾을 수 없게 된다. 이처럼 모든 상황에서 최적해를 찾을 수 있는 유연한 방법이 아니다.

동적 계획법과 비교된다.

동적 계획법(Dynamic Programming)이란, 전체를 바라보고 그것을 여러 개의 하위 문제들로 나누어 각 하위 문제들의 답을 이용해 최종 답을 내는 것이다.(복잡한 문제를 간단한 여러 개의 문제로 나누어 푸는 방법)

동적 계획법 특징

  • 큰 문제 안에 작은 문제가 중첩되어 있는 문제를 해결하는 데 사용. 예를 들어, 피보나치 수열.
  • 중첩되는 데이터라면 저장하고, 지속적으로 데이터를 참조한다.

피보나치 수열을 간단하게 코드화하면 아래와 같다.

1
2
3
4
5
public void fib(int n) {
if (n == 0) return 0;
else if (n == 1) return 1;
else return fib(n-1) + fib(n-2);
}

이때, fib(5)를 구하려고 하면 fib(2)의 계산은 여러번 중복된다. 이로 인한 계산 속도의 저하를 막기 위해 fib(2)와 같이 중복되는 값은 배열에 저장하여 필요할 때 배열에 접근해서 값을 가져오는 방식이다.

중복계산이 줄어들기 때문에 시간 복잡도는 O(n)가 된다.

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