3つの画像読み込みライブラリ Glide / Picasso / Coil - JetpackCompose 対応の状況

どれが、今、旬なのか。

まずはリンクを列挙しておきます。

全部使ってみようと思います。

giide vs picasso vs coil

👉 android glide, android picasso, android coil - 調べる - Google トレンド hatena-bookmark

 

Glide


32.8k stars
Watchers 1.1k watching
Forks 6k forks

👉 bumptech/glide: An image loading and caching library for Android focused on smooth scrolling hatena-bookmark

Glide

👉 Glide v4 : Fast and efficient image loading for Android hatena-bookmark

@sjudd Hey, I believe you're one of the maintainers of Glide. Is it a planned feature? Does Glide's team want help from the community on this? Jetpack Compose is going to stable soon, I believe later this month or next month

👉 Jetpack Compose Support · Issue #4459 · bumptech/glide hatena-bookmark

 

Picasso


18.3k stars
Watchers 867 watching
Forks 4k forks

👉 square/picasso: A powerful image downloading and caching library for Android hatena-bookmark

Picasso

👉 Picasso hatena-bookmark

Nobody works on Picasso. If you want something in the next N years definitely use Coil. Or Glide. Or whatever. They're all fine.

Image loading is a terrible, horrible business to be in. It's been really nice not being in that business for the last few years. I don't see a reason to resume. I certainly have no intent to support it anymore. Picasso accomplished its goal of moving the ecosystem out of the painful image loaders of 2011/2012 to the fluent and extensible ones we know today. But it's filled with technical debt and the legacy of poor design (at least, in hindsight) and is currently very, very stuck between a major refactor and redesign with no end in sight.

👉 Consider providing Jetpack Compose support · Issue #2203 · square/picasso hatena-bookmark

 

Coil


8.4k stars
Watchers 98 watching
Forks 512 forks

👉 coil-kt/coil: Image loading for Android backed by Kotlin Coroutines. hatena-bookmark

Coil

👉 Coil hatena-bookmark

To add support for Jetpack Compose, import the extension library:

implementation("io.coil-kt:coil-compose:2.1.0")

👉 Jetpack Compose - Coil hatena-bookmark

 

まとめ

JetpackCompose への対応ライブラリ群も見逃せません。

👉 skydoves/landscapist: 🍂 Jetpack Compose image loading library that fetches and displays network images with Glide, Coil, and Fresco hatena-bookmark

👉 wasabeef/composable-images: The Composable Images is a library providing Jetpack Compose wrapper for Glide, Picasso, and Coil. hatena-bookmark

アーキテクチャー、フレームワーク、ライブラリの選定はそのプロダクトの安定感に直結します。

機能確認を中心に検証しながら、将来の本筋を外してはなりません。

👉 画像読み込みライブラリ「COIL」 hatena-bookmark


【Jetpack Compose】Square 製 Radiography で View の構造を確認する

👉 square/radiography: Text-ray goggles for your Android UI. hatena-bookmark


dependencies {
  implementation 'androidx.compose.ui:ui-tooling:1.0.0-betaXY'
  implementation 'com.squareup.radiography:radiography:2.4.1'
}

scan() すると、画面のツリー構造がテキストで取得できるようでです。


DecorView { 1080×2160px }
├─LinearLayout { id:main, 1080×1962px }
│ ├─EditText { id:username, 580×124px, focused, text-length:0, ime-target }
│ ├─EditText { id:password, 580×124px, text-length:0 }
│ ╰─LinearLayout { 635×154px }
│   ├─Button { id:signin, 205×132px, text-length:7 }
│   ╰─Button { id:forgot_password, 430×132px, text-length:15 }
├─View { id:navigationBarBackground, 1080×132px }
╰─View { id:statusBarBackground, 1080×66px }


// Render the view hierarchy for all windows.
val prettyHierarchy = Radiography.scan()


// Extension function on View, renders starting from that view.
val prettyHierarchy = someView.scan()

 

Jetpack Compose でやってみる

この画面でやってみます。

square / radiography

Composable にコードを追加しておきます。


@Composable
fun HomeScreen(
  viewModel: HomeViewModel = hiltViewModel()
) {

  // ...

  val view: View = LocalView.current

  SideEffect {
    println(Radiography.scan())
    println(view.scan())
  }

}

👉 SideEffect - Compose における副作用  |  Jetpack Compose  |  Android Developers hatena-bookmark

 

結果


I: window-focus:false
I:  DecorView { 1080×2160px }
I:  ├─LinearLayout { 1080×2116px }
I:  │ ├─ViewStub { id:action_mode_bar_stub, GONE, 0×0px }
I:  │ ╰─FrameLayout { id:content, 1080×2039px }
I:  │   ╰─ComposeView { 1080×2039px }
I:  │     ╰─AndroidComposeView { 1080×2039px }
I:  │       ╰─CompositionLocalProvider { 1080×2039px }
I:  │         ╰─ScaffoldLayout { 1080×2039px }
I:  │           ├─<subcomposition of ScaffoldLayout>
I:  │           ├─<subcomposition of ScaffoldLayout>
I:  │           │ ╰─SnackbarHost
I:  │           ├─<subcomposition of ScaffoldLayout>
I:  │           ├─<subcomposition of ScaffoldLayout>
I:  │           │ ╰─CompositionLocalProvider { 1080×154px }
I:  │           │   ├─NavBottomBar { 1080×154px }
I:  │           │   │ ╰─Row { 1080×154px, SELECTABLE-GROUP }
I:  │           │   │   ├─BottomNavigationItem { 180×154px, roll:Tab, SELECTED }
I:  │           │   │   │ ╰─BottomNavigationTransition { 162×154px }
I:  │           │   │   │   ├─Box { 66×66px, layout-id:"icon" }
I:  │           │   │   │   │ ├─Icon
I:  │           │   │   │   │ │ ╰─RenderVectorGroup
I:  │           │   │   │   │ ╰─Icon { 66×66px }
I:  │           │   │   │   ╰─Box { 96×44px, layout-id:"label" }
I:  │           │   │   │     ╰─ProvideTextStyle { 96×44px, text-length:4 }
I:  │           │   │   ├─BottomNavigationItem { 180×154px, roll:Tab, SELECTED }
I:  │           │   │   │ ╰─BottomNavigationTransition { 151×154px }
I:  │           │   │   │   ├─Box { 66×66px, layout-id:"icon" }
I:  │           │   │   │   │ ├─Icon
I:  │           │   │   │   │ │ ╰─RenderVectorGroup
I:  │           │   │   │   │ ╰─Icon { 66×66px }
I:  │           │   │   │   ╰─Box { 85×44px, layout-id:"label" }
I:  │           │   │   │     ╰─ProvideTextStyle { 85×44px, text-length:5 }
I:  │           │   │   ├─BottomNavigationItem { 180×154px, roll:Tab, SELECTED }
I:  │           │   │   │ ╰─BottomNavigationTransition { 156×154px }
I:  │           │   │   │   ├─Box { 66×66px, layout-id:"icon" }
I:  │           │   │   │   │ ├─Icon
I:  │           │   │   │   │ │ ├─RenderVectorGroup
I:  │           │   │   │   │ │ ├─RenderVectorGroup
I:  │           │   │   │   │ │ ├─RenderVectorGroup
I:  │           │   │   │   │ │ ╰─RenderVectorGroup
I:  │           │   │   │   │ ╰─Icon { 66×66px }
I:  │           │   │   │   ╰─Box { 90×44px, layout-id:"label" }
I:  │           │   │   │     ╰─ProvideTextStyle { 90×44px, text-length:5 }
I:  │           │   │   ├─BottomNavigationItem { 180×154px, roll:Tab, SELECTED }
I:  │           │   │   │ ╰─BottomNavigationTransition { 150×154px }
I:  │           │   │   │   ├─Box { 66×66px, layout-id:"icon" }
I:  │           │   │   │   │ ├─Icon
I:  │           │   │   │   │ │ ╰─RenderVectorGroup
I:  │           │   │   │   │ ╰─Icon { 66×66px }
I:  │           │   │   │   ╰─Box { 84×44px, layout-id:"label" }
I:  │           │   │   │     ╰─ProvideTextStyle { 84×44px, text-length:4 }
I:  │           │   │   ├─BottomNavigationItem { 180×154px, roll:Tab, SELECTED }
I:  │           │   │   │ ╰─BottomNavigationTransition { 146×154px }
I:  │           │   │   │   ├─Box { 66×66px, layout-id:"icon" }
I:  │           │   │   │   │ ├─Icon
I:  │           │   │   │   │ │ ╰─RenderVectorGroup
I:  │           │   │   │   │ ╰─Icon { 66×66px }
I:  │           │   │   │   ╰─Box { 80×44px, layout-id:"label" }
I:  │           │   │   │     ╰─ProvideTextStyle { 80×44px, text-length:4 }
I:  │           │   │   ╰─BottomNavigationItem { 180×154px, roll:Tab, SELECTED }
I:  │           │   │     ╰─BottomNavigationTransition { 180×154px }
I:  │           │   │       ├─Box { 66×66px, layout-id:"icon" }
I:  │           │   │       │ ├─Icon
I:  │           │   │       │ │ ╰─RenderVectorGroup
I:  │           │   │       │ ╰─Icon { 66×66px }
I:  │           │   │       ╰─Box { 114×44px, layout-id:"label" }
I:  │           │   │         ╰─ProvideTextStyle { 114×44px, text-length:8 }
I:  │           │   ╰─AdaptiveAd { 1080×0px }
I:  │           │     ╰─ViewFactoryHolder { 1080×0px }
I:  │           │       ╰─AdView { 1080×0px }
I:  │           │         ╰─FrameLayout { 0×0px }
I:  │           ╰─<subcomposition of ScaffoldLayout>
I:  │             ╰─NavHost
I:  │               ╰─Box
I:  │                 ╰─LocalOwnersProvider
I:  │                   ├─TopBar
I:  │                   │ ╰─CompositionLocalProvider
I:  │                   │   ├─Row
I:  │                   │   │ ╰─CompositionLocalProvider { roll:Button }
I:  │                   │   │   ├─CompositionLocalProvider
I:  │                   │   │   │ ╰─RenderVectorGroup
I:  │                   │   │   ╰─CompositionLocalProvider
I:  │                   │   ├─Row
I:  │                   │   │ ╰─ProvideTextStyle { text-length:4 }
I:  │                   │   ╰─CompositionLocalProvider
I:  │                   ╰─Column
I:  │                     ├─Button { roll:Button }
I:  │                     │ ╰─CompositionLocalProvider
I:  │                     │   ╰─Text { text-length:7 }
I:  │                     ├─Spacer
I:  │                     ├─Button { roll:Button }
I:  │                     │ ╰─CompositionLocalProvider
I:  │                     │   ╰─Text { text-length:7 }
I:  │                     ├─Spacer
I:  │                     ├─Button { roll:Button }
I:  │                     │ ╰─CompositionLocalProvider
I:  │                     │   ╰─Text { text-length:7 }
I:  │                     ├─Spacer
I:  │                     ╰─Button { roll:Button }
I:  │                       ╰─CompositionLocalProvider
I:  │                         ╰─Text { text-length:5 }
I:  ├─View { id:navigationBarBackground, 1080×44px }
I:  ╰─View { id:statusBarBackground, 1080×77px }

I: AndroidComposeView:
I: window-focus:false
I:  AndroidComposeView { 1080×2039px }
I:  ╰─CompositionLocalProvider { 1080×2039px }
I:    ╰─ScaffoldLayout { 1080×2039px }
I:      ├─<subcomposition of ScaffoldLayout>
I:      ├─<subcomposition of ScaffoldLayout>
I:      │ ╰─SnackbarHost
I:      ├─<subcomposition of ScaffoldLayout>
I:      ├─<subcomposition of ScaffoldLayout>
I:      │ ╰─CompositionLocalProvider { 1080×154px }
I:      │   ├─NavBottomBar { 1080×154px }
I:      │   │ ╰─Row { 1080×154px, SELECTABLE-GROUP }
I:      │   │   ├─BottomNavigationItem { 180×154px, roll:Tab, SELECTED }
I:      │   │   │ ╰─BottomNavigationTransition { 162×154px }
I:      │   │   │   ├─Box { 66×66px, layout-id:"icon" }
I:      │   │   │   │ ├─Icon
I:      │   │   │   │ │ ╰─RenderVectorGroup
I:      │   │   │   │ ╰─Icon { 66×66px }
I:      │   │   │   ╰─Box { 96×44px, layout-id:"label" }
I:      │   │   │     ╰─ProvideTextStyle { 96×44px, text-length:4 }
I:      │   │   ├─BottomNavigationItem { 180×154px, roll:Tab, SELECTED }
I:      │   │   │ ╰─BottomNavigationTransition { 151×154px }
I:      │   │   │   ├─Box { 66×66px, layout-id:"icon" }
I:      │   │   │   │ ├─Icon
I:      │   │   │   │ │ ╰─RenderVectorGroup
I:      │   │   │   │ ╰─Icon { 66×66px }
I:      │   │   │   ╰─Box { 85×44px, layout-id:"label" }
I:      │   │   │     ╰─ProvideTextStyle { 85×44px, text-length:5 }
I:      │   │   ├─BottomNavigationItem { 180×154px, roll:Tab, SELECTED }
I:      │   │   │ ╰─BottomNavigationTransition { 156×154px }
I:      │   │   │   ├─Box { 66×66px, layout-id:"icon" }
I:      │   │   │   │ ├─Icon
I:      │   │   │   │ │ ├─RenderVectorGroup
I:      │   │   │   │ │ ├─RenderVectorGroup
I:      │   │   │   │ │ ├─RenderVectorGroup
I:      │   │   │   │ │ ╰─RenderVectorGroup
I:      │   │   │   │ ╰─Icon { 66×66px }
I:      │   │   │   ╰─Box { 90×44px, layout-id:"label" }
I:      │   │   │     ╰─ProvideTextStyle { 90×44px, text-length:5 }
I:      │   │   ├─BottomNavigationItem { 180×154px, roll:Tab, SELECTED }
I:      │   │   │ ╰─BottomNavigationTransition { 150×154px }
I:      │   │   │   ├─Box { 66×66px, layout-id:"icon" }
I:      │   │   │   │ ├─Icon
I:      │   │   │   │ │ ╰─RenderVectorGroup
I:      │   │   │   │ ╰─Icon { 66×66px }
I:      │   │   │   ╰─Box { 84×44px, layout-id:"label" }
I:      │   │   │     ╰─ProvideTextStyle { 84×44px, text-length:4 }
I:      │   │   ├─BottomNavigationItem { 180×154px, roll:Tab, SELECTED }
I:      │   │   │ ╰─BottomNavigationTransition { 146×154px }
I:      │   │   │   ├─Box { 66×66px, layout-id:"icon" }
I:      │   │   │   │ ├─Icon
I:      │   │   │   │ │ ╰─RenderVectorGroup
I:      │   │   │   │ ╰─Icon { 66×66px }
I:      │   │   │   ╰─Box { 80×44px, layout-id:"label" }
I:      │   │   │     ╰─ProvideTextStyle { 80×44px, text-length:4 }
I:      │   │   ╰─BottomNavigationItem { 180×154px, roll:Tab, SELECTED }
I:      │   │     ╰─BottomNavigationTransition { 180×154px }
I:      │   │       ├─Box { 66×66px, layout-id:"icon" }
I:      │   │       │ ├─Icon
I:      │   │       │ │ ╰─RenderVectorGroup
I:      │   │       │ ╰─Icon { 66×66px }
I:      │   │       ╰─Box { 114×44px, layout-id:"label" }
I:      │   │         ╰─ProvideTextStyle { 114×44px, text-length:8 }
I:      │   ╰─AdaptiveAd { 1080×0px }
I:      │     ╰─ViewFactoryHolder { 1080×0px }
I:      │       ╰─AdView { 1080×0px }
I:      │         ╰─FrameLayout { 0×0px }
I:      ╰─<subcomposition of ScaffoldLayout>
I:        ╰─NavHost
I:          ╰─Box
I:            ╰─LocalOwnersProvider
I:              ├─TopBar
I:              │ ╰─CompositionLocalProvider
I:              │   ├─Row
I:              │   │ ╰─CompositionLocalProvider { roll:Button }
I:              │   │   ├─CompositionLocalProvider
I:              │   │   │ ╰─RenderVectorGroup
I:              │   │   ╰─CompositionLocalProvider
I:              │   ├─Row
I:              │   │ ╰─ProvideTextStyle { text-length:4 }
I:              │   ╰─CompositionLocalProvider
I:              ╰─Column
I:                ├─Button { roll:Button }
I:                │ ╰─CompositionLocalProvider
I:                │   ╰─Text { text-length:7 }
I:                ├─Spacer
I:                ├─Button { roll:Button }
I:                │ ╰─CompositionLocalProvider
I:                │   ╰─Text { text-length:7 }
I:                ├─Spacer
I:                ├─Button { roll:Button }
I:                │ ╰─CompositionLocalProvider
I:                │   ╰─Text { text-length:7 }
I:                ├─Spacer
I:                ╰─Button { roll:Button }
I:                  ╰─CompositionLocalProvider
I:                    ╰─Text { text-length:5 }

どう使います?、これ。

scan() 時のオプションもいろいろあるようなので試してみますか。


【Jetpack Compose】ViewModel を捨てて Repository を Composable に直結する

 

気になるのはライフサイクル。

 

きっかけ

フル Compose でよくあるTodoのようなメモのようなアプリを作ってみました。

一通りの機能は実装しました。




👉 Jetpack Compose without ViewModel #shorts - YouTube hatena-bookmark

いろいろ Compose を試しながら進んでいくと ViewModel がスカスカになりました。


@HiltViewModel
class TodoViewModel @Inject constructor(
  private val repository: TodoRepositoryInterface
) : ViewModel() {

  val items: Flow<List<Todo>> = repository.load()

  fun insert(text: String) = repository.insert(text)

  fun update(id: Long, text: String) = repository.update(id, text)

  fun delete(id: Long) = repository.delete(id)

}

ViewModel いらなくね?

ViewModel を省略して、Repository を直結します。

 

結果

以下、少しの書き換えで問題なく動きます。


@Composable
fun TodoScreen(
  //viewModel: TodoViewModel = hiltViewModel()
  repository: TodoRepository = TodoRepository(
    Database(
      AndroidSqliteDriver(
        schema = Database.Schema,
        context = LocalContext.current,
        name = "database.db"
      )
    )
  )
) {


//val items by viewModel.items.collectAsState(initial = emptyList())
val items by repository.load().collectAsState(initial = emptyList())


//viewModel.insert(target.text)
repository.insert(target.text)


//viewModel.update(target.id, target.text) 
repository.update(target.id, target.text)


//viewModel.delete(target.id)
repository.delete(target.id)


//viewModel.delete(target.id)
repository.delete(target.id)

画面回転問題なし、メモリーリークもありません。

すんなりです。

Square製 SQLDelight + Flow(coroutine extension) を使っていますが、

Room + LiveData でもいけると思います。

Composable で Flow(LiveData) を受け取った瞬間に、

collectAsState(ObserveAsState) で State に変換できるんなら、

それのほうが良くね?

ライフサイクルの差も気にしなくていいし。

しかし、緩衝国がなくなるのはなんだか不安です。

Hilt で @Singleton で、ぶち込んでやりたかったです。

あ、でもこれ、 re-compose のたびに、Repository インスタンスが...

(つづく...)

👉 「SwiftUIでMVVMを採用するのは止めよう」と思い至った理由 - Qiita hatena-bookmark
👉 ViewModel はいつ生まれていつ死ぬか 【→ Jetpack Compose】 hatena-bookmark
👉 Jetpack ComposeとViewModelについて考える - Blog - Mori Atsushi hatena-bookmark


Kotlin 今どきよくある JSON リクエストからのパース

数年で一気に変わってます、JSONの取り扱い処理。

GsonMoshi も不要です。

Kotlin 内蔵の serialization を使うのが良いでしょう。

👉 Kotlin/kotlinx.serialization: Kotlin multiplatform / multi-format serialization hatena-bookmark


@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
  val contentType = "application/json".toMediaType()
  val json = Json { 
    ignoreUnknownKeys = true
    isLenient = true
  } // *
  return Retrofit.Builder()
    .client(okHttpClient)
    .baseUrl(BASE_URL)
    .addConverterFactory(json.asConverterFactory(contentType))
    .build()
}

処理時に便利に設定を変えられるようになっているので、

よくあるやつを並べておきます。

https://api.cryptowat.ch/markets/prices
👉 【仮想通貨】Cryptowatch Public Market REST API を眺める hatena-bookmark

 

ignoreUnknownKeys = true

デフォルトでは、逆シリアル化中に不明なキーが検出されるとエラーが発生します。 これを回避し、ignoreUnknownKeysプロパティをtrueに設定することで、このようなキーを無視できます。

👉 kotlinx.serialization/json.md at master · Kotlin/kotlinx.serialization hatena-bookmark

公開されている WEB-API には不要なデータがたくさんあります。

それを無視するための設定です。

 

isLenient = true

デフォルトでは、JsonパーサーはさまざまなJSON制限を強制して、可能な限り仕様に準拠します(RFC-4627を参照)。 特に、キーは引用符で囲まれている必要があり、リテラルは引用符で囲まれていない必要があります。 これらの制限は、isLenientプロパティを使用して緩和できます。 isLenient = trueを使用すると、非常に自由にフォーマットされたデータを解析できます。

私はどうしても必要なときにしか使いません。

RFCに基づかないJSONは一応留意しておきたいので。

👉 kotlinx.serialization/json.md at master · Kotlin/kotlinx.serialization hatena-bookmark
👉 RFC 4627 - The application/json Media Type for JavaScript Object Notation (JSON) hatena-bookmark

 

まとめ

一度、テンプレート化しておくと、当分使い回すことができます。

調べるときに、いろいろ古い情報が多くて時間かかったので、メモとして。

👉 【Retorofit】コピペで使える NetworkModule【Dagger Hilt】 hatena-bookmark
👉 Kotlin/kotlinx.serialization: Kotlin multiplatform / multi-format serialization hatena-bookmark



R8 で 難読化 された スタックトレース を元に戻す


minifyEnabled true

のとき、実機にインストールして起動した瞬間にクラッシュします。


2022-04-02 23:21:04.099 5125-5125/com.benigumo.example E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.benigumo.example, PID: 5125
    java.lang.NullPointerException
        at w5.c$c.b(:112)
        at w5.c$c.f(:154)
        at v5.f.d(:233)
        at e7.x$a.c(:46)
        at v5.b$a.a(:38)
        at e7.x$a.b(:43)
        at w5.b$b.a(:25)
        at w5.b$b.add(:23)
        at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:393)
        at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:133)
        at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:4847)
        at android.app.servertransaction.ResumeActivityItem.execute(ResumeActivityItem.java:54)
        at android.app.servertransaction.ActivityTransactionItem.execute(ActivityTransactionItem.java:45)
        at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2214)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loopOnce(Looper.java:201)
        at android.os.Looper.loop(Looper.java:288)
        at android.app.ActivityThread.main(ActivityThread.java:7842)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)

難読化されてるので原因を特定するのがツラいです !!

難読化を元に戻してみましょう。

 

手順

1. R8 ルールを追加する。

proguard-rules.pro に以下を追加することで再構成がより確実になります。


-keepattributes LineNumberTable,SourceFile
-renamesourcefileattribute SourceFile

2. 再度クラッシュさせる。

ルール追加したので再度クラッシュさせてスタックトレースを出力します。


2022-04-04 02:44:01.054 2001-2001/com.benigumo.example E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.benigumo.example, PID: 2001
    java.lang.NullPointerException
        at w5.c$c.b(SourceFile:112)
        at w5.c$c.f(SourceFile:154)
        at v5.f.d(SourceFile:233)
        at e7.x$a.c(SourceFile:46)
        at v5.b$a.a(SourceFile:38)
        at e7.x$a.b(SourceFile:43)
        at w5.b$b.a(SourceFile:25)
        at w5.b$b.add(SourceFile:23)
        at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:393)
        at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:133)
        at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:4847)
        at android.app.servertransaction.ResumeActivityItem.execute(ResumeActivityItem.java:54)
        at android.app.servertransaction.ActivityTransactionItem.execute(ActivityTransactionItem.java:45)
        at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2214)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loopOnce(Looper.java:201)
        at android.os.Looper.loop(Looper.java:288)
        at android.app.ActivityThread.main(ActivityThread.java:7842)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)

少しスタックトレースが変化しました。

これは、クリップボードにコピーして使います。

3. 難読化を解除して元に戻す。 - retrace

「スタックトレース」 ( クリップボード内 ) と

「マッピングファイル」
( <module>/build/outputs/mapping/<build>/mapping.txt )

をデータとして retrace ツールを実行します。


~/Example
❯ pbpaste | java -jar ~/Library/Android/sdk/tools/proguard/lib/retrace.jar -verbose \
app/build/outputs/mapping/debug/mapping.txt
 
2022-04-04 02:44:01.054 2001-2001/com.benigumo.example E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.benigumo.example, PID: 2001
    java.lang.NullPointerException
        at curtains.internal.WindowCallbackWrapper$Companion.android.view.Window$Callback getJetpackWrapped(android.view.Window$Callback)(SourceFile:112)
        at curtains.internal.WindowCallbackWrapper$Companion.android.view.Window$Callback unwrap(android.view.Window$Callback)(SourceFile:154)
        at curtains.WindowsKt.android.view.Window$Callback getWrappedCallback(android.view.Window$Callback)(SourceFile:233)
        at leakcanary.RootViewWatcher$listener$1.void onRootViewAdded(android.view.View)(SourceFile:46)
        at curtains.OnRootViewAddedListener$DefaultImpls.void onRootViewsChanged(curtains.OnRootViewAddedListener,android.view.View,boolean)(SourceFile:38)
        at leakcanary.RootViewWatcher$listener$1.void onRootViewsChanged(android.view.View,boolean)(SourceFile:43)
        at curtains.internal.RootViewsSpy$delegatingViewList$1.boolean add(android.view.View)(SourceFile:25)
        at curtains.internal.RootViewsSpy$delegatingViewList$1.boolean add(java.lang.Object)(SourceFile:23)
        at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:393)
        at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:133)
        at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:4847)
        at android.app.servertransaction.ResumeActivityItem.execute(ResumeActivityItem.java:54)
        at android.app.servertransaction.ActivityTransactionItem.execute(ActivityTransactionItem.java:45)
        at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2214)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loopOnce(Looper.java:201)
        at android.os.Looper.loop(Looper.java:288)
        at android.app.ActivityThread.main(ActivityThread.java:7842)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)

全クラス名が丸出しになりました !!

👉 R8 retrace  |  Android デベロッパー  |  Android Developers 

4. Android Studio でスタックトレースを分析します。


[Analyze] - [Stack Trace or Thread Dump ...]

[Analyze] - [Stack Trace or Thread Dump ...]

[Analyze] - [Stack Trace or Thread Dump ...]

ソースファイルへのクリック可能なリンクも表示されています。

👉 スタック トレースを分析する  |  Android デベロッパー  |  Android Developers 

ちなみに、今回のクラッシュの原因は、Leakcanary まわりでした !


Proguard GUI

同様に、同梱の Proguard GUI を使っても retrace はできます。


java -jar ~/Library/Android/sdk/tools/proguard/lib/proguardgui.jar

GUI上でマッピングファイルのパスを入力して、スタックトレースは貼り付けます。

proguardgui.jar

 

日本語

さて、「難読化 (obfuscation)」の反対語は何なのでしょうか。

英語だと

「deobfuscation」

ですが、公式リファレンスの日本語を見ているといろいろな単語が使われています。

「デコード」

「難読化の解除」

「解読」

日本語は難しいですね !

 

まとめ

proguard-rules.pro に追記して、コピーしたスタックトレースを2つのツールどちらかで retrace する。


# To de-obfuscate stacktrace
-keepattributes LineNumberTable,SourceFile
-renamesourcefileattribute SourceFile


pbpaste | java -jar ~/Library/Android/sdk/tools/proguard/lib/retrace.jar -verbose \
app/build/outputs/mapping/debug/mapping.txt | tee >(pbcopy)


java -jar ~/Library/Android/sdk/tools/proguard/lib/proguardgui.jar

alias にしとくと良いでしょうか。

👉 アプリの圧縮、難読化、最適化  |  Android デベロッパー  |  Android Developers 
👉 【Android Studio】プラグイン「Proguard Retrace Unscrambler」で難読化されたスタックトレースを解除してみた