[후기] 안드로이드 뜻밖의 역사

이 책은 Androids: The Team that Built the Android Operating System 전문을 번역한 책이며, 영문으로는 해당 링크에서 무료로 볼 수 있다.

  • Read started from 2022.08.26
  • 출간일: 2022.08.19
  • 저자: 쳇 하스
  • 출판사: 인사이트

-

나는 이 책을 기술적인 세부 내용을 아는 소프트웨어/하드웨어 엔지니어 말고도 누구나 읽기를 바란다._1부 中

책을 읽기 전 팔락팔락 넘기다가 SCSI 단어가 나오길래 비관련 종사자는 읽기 힘들거라고 생각했는데, 책을 처음부터 읽는다면 미리 설명한 사람들이 나와서 알만한 이야기를 한다. 이 책에서 나는,

  1. 안드로이드 1.0을 릴리즈하기 위한 열정
  2. 안드로이드 내부 기능 각각이 탑재된 배경
  3. 안드로이드 OS가 시장에 나오기까지의 과정

세 가지를 중점적으로 봤기 때문에 이런 부분을 기대한다면 읽기 불편할 것 같진 않다고 생각했다.

첫 번째, 열정.

다 읽고 나면 안드로이드라는 새로운 운영 체제를 위해 함께 토론하고 결정하며 달려온 기분을 맛볼 수 있다.

역사를 공유하고 있다는 확고한 느낌이 있었죠._1장 中

이 책을 읽은 시기가 마침 회사에서 안드로이드 개발자로 일하면서 두 가지를 고민하던 때였다.

내가 ‘왜’ 여기 있는 거지?

물론 안드로이드 개발을 하고 있었으나 나는 내가 좋아해서 고른 이 직무에 관해 의구심이 생겼다.

‘나는 왜 안드로이드를 좋아했던 거지?’

지금도 좋아하지 않는 게 아니었다. 그런데 처음 이끌렸던 이유가 기억나지 않았다. 분명히 있을 거라고 생각했고, 나는 지금까지 인생을 회고하면서 그걸 찾아보고자 했다. 그런데 이 책은 표지처럼 안드로이드의 골대를 세우고, 내부 톱니바퀴를 채우고, 외피를 덮는 공정을 밟는다. 그게 뜻밖에도 나에게 도움을 주었다.

자세한 건 여기에 적지 않겠으나, 첫 직장에서 C#을 했었고, 거기서 마음에 들었던 부분이 안드로이드에 있었다는 걸 이 책에서 안드로이드의 내부를 하나하나 짚어주는 걸 따라가면서 발견해냈던 것이다.

그런 점에서 나는 이 책을 읽기 잘했다는 마음이 들었고, 끝까지 재미있게 읽을 수 있었다.

두 번째, 배경.

안드로이드의 각 내부기능, 예를 들면 알람, 지도, 웹뷰는 물론이고 선택된 언어, 저장장치 구성, 이미지 렌더링에 관하여 코드로써 다뤄본 적이 있다면 더욱 빠져들 수 있을 것이다.

참고로, 현재 Android Developer의 플랫폼 아키텍처에서는 소프트웨어 스택(흔히 말하길 ‘안드로이드 운영체제 구조’)을 아래와 같이 그림으로 나타내고 있다.

Android 소프트웨어 스택

그러나 이건 런타임이 ART(Android 5.0(API 레벨 21) 이상이 실행되는 기기의 기본 런타임)로 바뀐 2022년 버전 구조이고, 이 책에서는 1.0이하를 중점적으로 다루고 있으므로 아래와 같이 런타임에 Dalvik을 사용하는 구조에 가깝다고 볼 수 있겠다.

현업 종사자라면 두 사진을 비교해보는 것도 소소한 즐거움이 되지 않을까.

Android 운영체제 구조

그림 출처: Anatomy Physiology of an Android (Google IO 2008)

“누군가 그걸 좋아했기 때문이 아니라 플랫폼이 성공하려면 타당했기 때문이고 팀이 거기에 적응한 거예요.”_8장 中

의사결정 과정에 대해 다이앤이 한 말이다. 기능의 세부사항들은 모두 당시에 타당한 기준으로 선별되었고, 구성하였음을 알 수 있다.

책에서 재미있게 읽고 기억에 남았던 게 몇 가지 있다.

  • GPU없이 렌더링되던 시절의 이야기
  • 초기 운영체제 모델로 오른 액티비티와 main() 함수
  • ART 런타임으로 바뀌기 이전의 초기 런타임 형태
  • View의 스레드 측면 작동 방식

포트 5228에 대한 내용도 잠깐 있었고 내용이 정말 알찼다. 안드로이드의 기능이나 구조를 한 번씩 언급해준다. 개인적으로 웹뷰에 대한 이야기가 길게 이어져서 좋았고, 업데이트 중 메모리 확보 및 보안을 신경쓴 부분도 자세하게 나와서 읽는 재미가 있었다.

1.0 릴리즈까지 타이트한 일정이었으나 결국 제품을 완성하여 최고의 타이밍에 출시한 안드로이드. <세상을 빛낸 Geek>에서는 맥의 화려함을 강조한 모양이나, 나는 <안드로이드 뜻밖의 역사>가 좋다.

안드로이드가 오픈 소스인 점, 앱스토어에서 사용자를 가리지 않은 점. 모두에게 열려있다는 인상, 그리고 개발자 친화적이라는 생각도 들었다.

나는 Android가 좋다:)

다음은 1부에 나오는 에번 밀러의 말을 옮긴 것이다. 이를 마지막으로 후기를 마무리하고자 한다.

“처음부터 필연 같은 건 없었어요.
안드로이드가 성공하지 못할 이유는 많았죠.
똑같은 일을 다시 이뤄 내고 싶다고 해도 할 수 없을 거예요.
뭔가 마법이 벌어진 거죠.”

-

Android

[Microsoft] Java 11 이상으로 이동하는 이유

  • 2022.05.20 작성됨

Java 8에 대한 업데이트가 유료로 전환되어 Microsoft에서는 빠른 시일 내에 11 버전으로 옮기길 권장하고 있다.

Java 11로 업그레이드할 경우 장점으로는

  • 모듈: 메모리를 더 적게 쓰고, 클래스 로딩이 빨라진다.
  • GC: Java 8에서는 여러 스레드를 사용하는 병렬 GC를 사용해 속도를 높인 반면, Java 11에서는 G1 가비지 수집기를 사용해 처리량을 높였다.

Android Studio Arctic Fox(2020.3.1)

An exception occurred applying plugin request [id: ‘com.android.library’]
Failed to apply plugin ‘com.android.internal.library’.
Android Gradle plugin requires Java 11 to run. You are currently using Java 1.8.

Arctic Fox는 JDK 11 기반으로 되어있어 Gradle도 7.0.x 버전으로 올라갔다. (기존 Android Studio 4.x는 JDK 1.8 기반) 이에 따라 에러 메세지에서는 ‘com.android.library’ 플러그인을 사용하는 데 문제가 있다고 명시해주고 있다.

2022~2023년 Kotlin 팀의 계획: 주요 프로젝트 및 생산성 기능

2022~2023년 핵심 프로젝트는 다음과 같습니다.

  • Kotlin 릴리스의 품질과 안정성 개선
  • K2 컴파일러의 베타 버전 출시
  • Kotlin Multiplatform Mobile의 안정화 버전 출시
  • K2 지원 플러그인을 포함한 Kotlin IntelliJ IDEA 플러그인의 알파 버전 출시
  • Kotlin/JS IR 백엔드의 안정화 버전 출시

2022 구글 I/O

안드로이드 로드맵 2022

안드로이드 버전 점유율(statistics)

Android Studio > Create New Project

Airbnb의 Server-Driven UI 시스템

Ghost Platform(GP)로 안드로이드의 업데이트가 느리다는 단점을 상쇄하기 위해 UI를 SDUI로 만드는 데에 도움을 주는 플랫폼을 AirBnb에서 만들어 사용하고 있다.

I'm eating a book

안드로이드 프로그래밍 Next Step

  • Read started from 2022.06.18
  • 종이책 출간일: 2017.06.05
  • 저자: 노재춘

이게 2017년 책이라 오래 됐지만, dumpsys를 사용하는 예시를 좀 더 알고 싶어서 관련 책을 찾다가 서점에서 파라락 보고 훌쩍 사왔다. 그런데 생각지도 못하게 4, 5장에서 Context와 Task에 관해 좀 더 자세히 알게 되었다.

4장 Context

  • ContextWrapper/ContextImpl와 Activity/Service/Application에 관한 관계성
  • 사용 가능한 Context의 차이점: Activity, getBaseContext(), getApplicationContext()

5장 Task 및 dumsys

  • Task에 관한 설명은 기본적인 dumpsys 사용법 설명에서 얻어걸렸다. adb shell dumpsys activity a라는 명령어의 매우 기다란 결과 로그 중 기본적인 키워드를 설명해준다.
  • Task 속성: singleTop, singleTask 등 Manifest의 launchMode 속성 및 Intent 플러그 설명이 태스크에 관한 설명과 콜라보되어 동작이 자세히 나와 있다.

그 외 장

  • 위 명령어 말고도 dumpsys의 다양한 옵션에 대한 예시가 있어 어떤 경우에 어떤 옵션으로 명령어를 사용하면 될지 감을 잡는 데에 개인적으로 도움이 됐다.

실무에 바로 적용하는 안드로이드 프로그래밍

  • Read started from 2022.05.03
  • 종이책 출간일: 2021.03.25
  • 저자: 크리스틴 마시케노, 브라이언 가드너, 빌 필립스, 크리스 스튜어트

액티비티의 생명주기를 State를 기준으로 표현한 다이어그램은 기존 Android Developer에 있는 것보다 훨씬 보기 좋더라.

전체적으로 약간 딥한 부분까지 설명이 잘 되어있다.
예를 들어, onSaveInstanceState를 액티비티 레코드와 같이 설명해주거나, MVVM 뷰모델 vs Jetpack ViewModel의 차이를 설명해주는 등 상세하고 최신 내용이 잘 반영되어있다. 예제 코드가 모두 코틀린으로 되어있는 점도 그렇다.

안드로이드를 공부하는 사람이라면 기본서 다음 두 번째 책으로 적절할 것 같다. 안드로이드/자바/코틀린의 기초 문법을 설명해주지는 않아서.

이 책에는 외부 라이브러리를 사용한 예제는 테스팅 코드를 제외하고는 없다. 의존성 주입을 위한 Dagger2나 Lint 예제는 <아키텍처를 알아야 앱 개발이 보인다>(옥수환)에 있으니 참고 하시기.

코틀린 완벽 가이드

  • Read started from 2022.04.01
  • 종이책 출간일: 2022.03.18
  • 저자: 알렉세이 세두노프

코틀린에 대해 하나부터 열까지 상세한 코드 예제와 함께 설명이 수록되어있어 읽는데 막힘 없이 읽을 수 있어 코틀린 입문에 좋을 것으로 보인다.

구체적으로는 아래 항목이 있다.

  1. 데이터 타입 다루는 법
  2. 각종 클래스의 특징
  3. 제네릭
  4. 자바와의 상호 운용성 (아주 친절한 설명!)
  5. 동시성
  6. 테스팅

기본을 탄탄히 하는 데는 Kotlin Guide보다 한국어라는 점에서 책을 보는 게 더 나을 것 같다. 코틀린에 관해 원하는 것은 대부분 이 책에서 얻을 수 있으리라..

덧붙여 이 책은 Kotlin 1.6을 기준으로 쓰였으며 어느 하위 버전에서 어떤 업데이트가 있었는지도 중요한 부분은 설명이 되어있다.

[Android / Glide] Glide 이미지 로딩 중 다른 scaleType 적용하기

Glide 동작을 테스트하면서 아래와 같은 상황이 발생하였다.

  1. 기존에 사용하던 로딩이미지의 ScaleType은 CENTER를 사용해야 한다.
  2. Glide로 로드한 이미지는 로딩이미지와는 다른 각자의 ScaleType을 가지고 있다.

따라서 loadingImage와 loadedImage의 ScaleType을 외부에서 설정만 해주면 내부에서 자동으로 바꿔서 보여주도록 만들고자 했다.

이 작업을 위해 Glide에서는 이미지 로딩 중 placeholder 노출, 리소스 로드 등의 동작을 확장 구현할 수 있도록 Target을 제공한다는 점을 먼저 말해두겠다. 이 Target은 into()를 통해 전달할 수 있다.

1
2
3
4
5
6
Target<Drawable> target =
Glide.with(fragment)
.load(url)
.into(new Target<Drawable>() {
...
});

이러한 Target를 상속받는 클래스를 만들어서 placeholder는 onLoadStarted, error는 onLoadFailed에 각각 정의하려고 한다. 이때 로딩이미지는 drawable 리소스로 앱에 저장되어있기 때문에 DrawableImageViewTarget을 상속받아 만든 게 아래와 같다.

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
public class ScaleLoadingImageViewTarget extends DrawableImageViewTarget {
private ImageView.ScaleType mLoadingScaleType;
private ImageView.ScaleType mReadyScaleType;

 // RequestOptions에서 정의해둔 scale은 fitCenter
public ScaleLoadingImageViewTarget(ImageView view, ImageView.ScaleType scaleType) {
super(view);
// loadingCenter()에서 받아온 scale
mLoadingScaleType = scaleType;
        // xml에서 정의해둔 scale
// (따로 정의되지 않았다면 여기에 RequestOptions scaleType인 fitCenter가 들어옴
mReadyScaleType = view.getScaleType();
}

@Override
public void onLoadStarted(@Nullable Drawable placeholder) {
ImageView imageView = getView();
imageView.setScaleType(mLoadingScaleType);
super.onLoadStarted(placeholder);
}

@Override
public void onLoadFailed(@Nullable Drawable errorDrawable) {
ImageView imageView = getView();
imageView.setScaleType(mLoadingScaleType);
super.onLoadFailed(errorDrawable);
}

    @Override
public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
ImageView imageView = getView();
imageView.setImageResource(0); // 로딩 이미지가 보여지고 있을 때 scaleType이 바뀌면서 이미지가 덜그럭 거리는 이슈 방어
imageView.setScaleType(mReadyScaleType);
super.onResourceReady(resource, transition);
}

@NonNull
public static ScaleLoadingImageViewTarget loadingCenter(ImageView imageView) {
return new ScaleLoadingImageViewTarget(imageView, ImageView.ScaleType.CENTER);
}
}

loadingCenter 메소드를 통해 위에 새롭게 정의된 Target에 접근할 수 있도록 한다.

1
2
3
4
5
Glide.with(context)
.load(url)
.listener(listener)
.apply(options)
.into(ScaleLoadingImageViewTarget.loadingCenter(imageView));

참고링크

그러나 이 방식을 적용한 ImageView가 담긴 RecyclerView를 구현하게 되면, RecyclerView 각 아이템이 재사용되면서 ScaleLoadingImageViewTarget의 생성자에서 호출하는 view.getScaleType()의 값이 기대한 대로 나오지 않는 경우가 있어 문제가 될 수 있다.

따라서 이를 아래와 같이 view의 ScaleType을 명시적으로 받도록 변경하였다.

1
2
3
4
5
Glide.with(context)
.load(url)
.listener(listener)
.apply(options)
.into(ScaleLoadingImageViewTarget.loadingCenter(imageView, ScaleType.FIT_CENTER));
1
2
3
4
5
6
7
8
9
10
public ScaleLoadingImageViewTarget(ImageView view, ImageView.ScaleType originScaleType, ImageView.ScaleType loadingScaleType) {
super(view);
mLoadingScaleType = loadingScaleType;
mReadyScaleType = originScaleType;
}

@NonNull
public static ScaleLoadingImageViewTarget loadingCenter(ImageView imageView, ImageView.ScaleType originScaleType) {
return new ScaleLoadingImageViewTarget(imageView, originScaleType, ImageView.ScaleType.CENTER);
}

[Kotlin] Ticker(티커) 모드 차이(TickerMode.FIXED_PERIOD, FIXED_DELAY)

개발 환경: Kotlin 1.6

coroutines 라이브러리에는 **티커(ticker)**라고 하는 특별한 랑데부 채널이 있다. 이 채널은 Unit 값을 계속 발생시키되 한 원소와 다음 원소의 발생 시점이 주어진 지연 시간만큼 떨어져 있는 스트림을 만든다.

여기서 랑데부 채널이란 내부 버퍼가 없어 이 채널에서의 send() 호출은 다른 어떤 코루틴이 receive()를 호출할 때까지 항상 일시 중단된다. 마찬가지로 receive() 호출은 다른 어떤 코루틴이 send()를 호출할 때까지 일시 중단되는 특성을 가진다.
즉, 랑데부 채널은 생산자와 소비자 코루틴이 교대로 활성화되도록 보장한다.

티커 채널을 만들려면 ticker()라는 함수를 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*

fun main() = runBlocking {
val ticker = ticker(100) // TickerMode.FIXED_PERIOD
println(withTimeoutOrNull(50) { ticker.receive() })
println(withTimeoutOrNull(60) { ticker.receive() })
delay(250)
println(withTimeoutOrNull(1) { ticker.receive() })
println(withTimeoutOrNull(60) { ticker.receive() })
println(withTimeoutOrNull(60) { ticker.receive() })
}
1
2
3
4
5
null
kotlin.Unit
kotlin.Unit
kotlin.Unit
null

TickerMode.FIXED_PERIOD

  1. 0ms ~ 50ms(타임아웃): 50ms(밀리초) 내에 티커 신호를 받으려고 시도하나, 티커 지연 시간이 100ms이므로 withTimeoutOtNull()은 신호를 받지 못하고 타임아웃이 걸려 널을 반환한다.

  2. 50ms ~ 100ms: 타임아웃이 1회 난 후 다음 60ms 안에 신호를 받으려고 시도한다. 그리고 이번에는 50 + 60ms가 100ms 보다 길기 때문에 결괏값을 얻는다. receive()가 호출되면 티커가 재개된다.

    2_1. 100ms ~ 350ms: 이때 소비자 코루틴이 약 250ms 동안 일시 중단된다. 일시 중단으로부터 100ms 후에 티커는 다른 신호를 보내고 신호가 수신될 때까지 일시 중단된다. 그리고 소비자 코루틴과 티커 코루틴 모두 150ms 동안 일시 중단 상태로 남는다.

  3. 350ms: 소비자 코루틴이 재개되고 신호를 요청하려고 시도한다. 신호가 이미 보내졌기 때문에 receive()는 즉시 결과를 반환한다.

    3_1. 이제 티커는 마지막 신호를 보내고 나서 얼마나 시간이 지났는지 검사하고(250ms), 지연 시간을 50ms로 줄인다.

  4. 350ms ~ 400ms: 소비자는 50ms 타임아웃 안에 신호를 받으려고 시도한다. 다음 신호가 50ms 이전에 보내졌기 때문에 이 시도는 거의 확실히 성공할 것이다.

  5. 400ms ~ 460ms(타임아웃): 마지막으로, 신호를 받으려는 receive() 호출이 거의 즉시 일어난다. 따라서 티커는 전체 지연 시간(100ms)를 다시 기다린다. 그 결과, 마지막 receive() 호출은 60ms 타임아웃 안에 티커로부터 신호를 받지 못하기 때문에 널을 받는다.

티커 모드를 FIXED_DELAY로 고정하면 결과가 다음과 같이 바뀐다.

1
2
3
4
5
null
kotlin.Unit
kotlin.Unit
null
kotlin.Unit

TickerMode.FIXED_DELAY

초반부는 앞의 예제와 비슷하게 진행된다. 하지만 250밀리초의 긴 지연 이후 소비자 코루틴이 재개될 때부터는 동작이 달라진다.

  1. 350ms: 소비자 코루틴이 재개되고 신호를 요청하려고 시도한다. 신호가 이미 보내졌기 때문에 receive()는 즉시 결과를 반환한다.

    3_1. receive()로 결과를 넘긴 시점에서 티커는 현재시간을 고려하지 않고 여기서부터 100ms를 다시 기다린다.

  2. 350ms ~ 410ms(타임아웃): 티커가 신호를 보내려면 40ms 남았으므로 널을 받는다.

  3. 410ms ~ 450ms: 3_1에서 티커가 재개된 시간으로부터 100ms가 지났으므로 결과를 무사히 반환받는다.

참고
알렉세이 세두노프 <코틀린 완벽 가이드>