Android2021. 6. 7. 15:29

 

사용자의 단말을 특정하기 위해서 예전부터 사용해오던 것들은 점차적으로 구글이 보안을 강화하는 정책으로 변경되면서 사용이 불가하게 변화하고 있다.

 

deviceId, imei, meid, ANDROID_ID, SERIAL, MAC address 등이 많이 사용되어 왔는데

대부분 deprecated되었고 ANDROID_ID는 기기 재설정시 변경되고 SERIAL은 Android10부터 'unknown'을 반환한다.

 

2021년 현재는 SSAID가 대체재로서 사용 가능하다.

동일단말에서 같은 개발자의 키스토어로 빌드된 앱들 끼리는 같은 SSAID를 사용할 수 있다. 

 

아래 코드는 안드로이드 버번과 상관없이 항상 유니크한 아이디를 반환해주는 메서드이다.

 

 

    private fun getUniqueDeviceIdentifier(): String? {
        val telephonyManager: TelephonyManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
        var uniqueId: String? = null
        try {
            telephonyManager.deviceId?.let {
                uniqueId = it
            }
            if (uniqueId==null && Build.VERSION.SDK_INT > Build.VERSION_CODES.O) {
                telephonyManager.imei?.let { uniqueId = it }
                if (uniqueId==null) {
                    telephonyManager.meid?.let { uniqueId = it }
                }
            }
        } catch(e: Exception) {}

        if (uniqueId==null) {
            uniqueId = Build.SERIAL
        }
        if (uniqueId==null || uniqueId=="unknown") {
            val wifiManager: WifiManager = getSystemService(Context.WIFI_SERVICE) as WifiManager
            val wifiInfo = wifiManager.connectionInfo
            uniqueId = wifiInfo.macAddress
        }
        if (uniqueId==null || uniqueId=="02:00:00:00:00:00") {
            uniqueId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)
        }
        return uniqueId
    }

 

Posted by 삼스
Android2021. 6. 2. 13:34


비동기로 파일을 다운로드 후 해당 파일을 로드하는 로직을 코루틴으로 작성하면 아래와 비슷한 로직으로 구성이 될것이다.

CoroutineScope(Dispatchers.IO).launch { // CoroutineScope를 IO스레드에서 launch
    if (downloadAsync(filePath, fileUrl)) { // 다운로드 코루틴메서드 다른 쓰레드에 영향을 주지 않는다.
        loadFromLocal(file, loadListener)  // 다운로드가 성공하면 다운로드된 파일을 로드한다.
    }
}


downloadAsync가 파일을 다운로드하는 코루틴 메서드로 suspend키워드와 coroutineScope을 지정하여 
다른 스레드에 영향을 주지 않고 동작하게 한다.


suspend fun downloadAsync(filePath: String, fileUrl: String): Boolean = coroutineScope {
    if (L.DBG) {
        Log.v(TAG, "downloadAsync : $filePath, $fileUrl")
    }
    return@coroutineScope download(filePath, fileUrl)
}

이걸 CompletableFuture에서 사용하고자 한다면 아래와 같은 형태가 될것이다.

if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
downloadAsync2(filePath, fileUrl)
        .thenAccept { r ->
            if (r) {
                loadFromLocal(file, loadListener)
            }
        }
}


downloadAsync2는 CompletableFuture를 반환하며 thenAccept로 콜백을 받아서 처리할 수 있다.

@RequiresApi(Build.VERSION_CODES.N)
fun downloadAsync2(filePath: String, fileUrl: String): CompletableFuture<Boolean> {
    return CompletableFuture.supplyAsync {
        return@supplyAsync download(filePath, fileUrl)
    }
}

 

두가지 코드는 완전히 동일하게 동작한다.
어떤게 더 좋아 보이는가??
난 개인적으로 코루틴이 나아 보인다.
왜냐하면 CompletableFuture의 경우 안드로이드 N부터 지원하는데다 코루틴이 좀더 사용이 쉽다.

Posted by 삼스
Android2021. 5. 27. 14:24


고전적인 비동기 처리라 하면 Thread와 mutex, semaphore를 활용한 방법이 있겠다.
이어 개발자들은 비동기 처리를 좀더 쉽게 하기 위해 나온게 Rx시리즈가 있는데 이는 데이터 흐름과 함수형프로그래밍의 관점에서 다루어서 더 인기가 있게 되었다.
이어서 일부 언어에서는 코루틴을 지원하기 시작했고 안드로이드의 새로운 개발언어인 kotlin도 지원하기에 이르렀다.
c#, javascript, go등의 언어에서 await, async를 사용해보았다면 이미 알고 있는 것이다.

스레드로 비동기적이면서 순차적인 작업을 하려고 하면 콜백을 사용하게 마련이고 이런 경우 단계가 늘어날수록 콜백의 늪에 빠지게 된다.

이를 좀더 이해하기 쉽게 코딩할수 있도록 지원한게 Rx시리즈이다.
순차적으로 파이프라인을 이어서 다음 작업을 진행할 수 있다.

그런데 Rx는 학습곡선이 좀 있다. 그리고 원하는 기능을 구현하기 위해서는 실제 작업을 수행하는 루틴과 별도로 부수적이 액세서리 코드들이 더 들어가게 된다.

코루틴은 이를 더 쉽고 간결하게 표한하게 해주며 코드의 가독성도 높여준다.

코루틴이 어떻게 이를 지원하는지에 대해서 먼저 간단하게 설명하겠다.

일반적인 루틴은 호출되면 프로그램의 컨텍스트가 해당 서브루틴으로 넘어가며 서브루틴에서 return될때 호출한 메인 루틴으로 컨텍스트가 복구된다. 이는 예상하는것보다 많은 비용이 수반된다.
코루틴은 메인루틴에서 코루틴이 호출될때 바로 빠져나오게 된다. 그리고 해당 코루틴이 끝나면 코루틴이 호출되었던 부분으로 다시 돌아올수 있다.

일반적인 루틴과 어떤 차이가 있는지에 대해 쉽게 설명하자면 서브루틴이 아주 오래걸리는 작업을 수행하는데 이 루틴이 메인UI스레드에서 동작중이라면 해당 작업이 완료되기전까지 화면이 멈추게 된다.
하지만 코루틴의 이 경우에도 화면이 멈추지 않고 다른 스레드는 돌아가게 된다. 

코루틴이 호출될때 별도의 스레드로 분리되어 호출되고 코드는 더 진행되지 않고 대기하는데 다른 스레드에 영향을 주지 않으면서 대기하다가 코루틴이 완료되면 다시 대기하였던 부분부터 수행된다고 생각하면 된다.

c#등의 언어로 아래와 같은 코드에 해당한다.

  await doSomething();

개발자들은 코드로 설명하는것이 항상 옳다.
thread, RxJava, coroutine으로 동일한 작업을 할 때의 차이를 코드로 살펴보자.

라면을 끓여 먹는다고 하자

라면을 사서 -> 냄비를 준비하고 -> 물을 부어 끊이고 -> 면과 스프를 넣어 끓인 후 -> 그릇에 담아서 -> 먹는다.

위 과정은 모두 순차적으로 진행해야함 최종적으로 내가 먹을 수 있다.
라면을 사지 않고 라면을 먹을수는 없다.

이를 비동기로 구현하려면 thread 호출로 해야 할것이고 이 경우 콜백형태를 사용할것이고

buyRamyeon(money) { ramyeon -> 
  getPot(ramyeon) { ramyeon, pot ->
    doBoilPot(ramyeon, pot) { cookedRamyeon ->
       moveToDish(cookedRamyeon) { ramyeonInDish ->
          eatIt(ramyeonInDish)
       }
    }
  }
}



이렇게 작업해도 문제는 없다. 다만 많은 비용을 수반하고 우리가 그 동안 많이 보아온 콜백지옥의 향연이 펼쳐진다.

이를 RxJava로 구현한다고 하면 아래와 비슷할것이다.

Observable.just(money)
.observeOn(MAIN_Thread)
.subscribeOn(IO_Thread)
.flatMap { money -> buyRamyeon(money) }
.flatMap { ramyeon -> getPot(ramyeon) }
.flatMap { ramyeon, pot -> doBoilPot(ramyeon, pot) }
.flatMap { ramyeonInDish -> moveToDish(cookedRamyeon) }
.subscribe({ 
ramyeonInDish -> eatIt(ramyeonInDish)
}, {
// 실패케이스 처리
})



콜백헬에서는 확실히 벗어난듯 하다.
하지만 just, observeOn, subscribeOn등에 대해 알아야 하며 Rx는 상당히 급한 학습곡선에 해당한다.

이를 coroutine으로 구현한다면 아래와 비슷할것이다.

suspend iWantEatRamyeon(money) {
  try {
    val ramyeon = buyRamyeon(money)
    val pot = getPot()
    val cookedRamyeon = doBoilPot(ramyeon, pot)
    val ramyeonInDish = moveToDish(cookedRamyeon)
    eatIt(ramyeonInDish)
  } catch (e: Exception) {
    // 실패케이스 처리
  }
}


Coroutine 문법을 자세히 아라보려면 https://selfish-developer.com/entry/Kotlin-Coroutine 여그를 보자. 잘 정리되어 있넹 

Posted by 삼스