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」で難読化されたスタックトレースを解除してみた 


build.gadle* に 'android-library' や 'kotlin-kapt' があったらやばい!

あるライブラリツールに Jake が反応しています。


👉 (1) Sam EdwardsさんはTwitterを使っています 「Looks like @AutonomousApps used "com.autonomousapps.dependency-analysis" for https://t.co/9rXv9Yb5tm But then there is "java-library", "kotlin-kapt", etc, etc. 🤔 https://t.co/yUFrUc2KLc」 / Twitter 

私的には、android-library はなかったものの kotlin-kapt はありました。

gradle 周りの記述は、どうしてもメンテが雑になっていたりします。

GitHub で検索してみます。

結構ヒットします。


👉 Search · android-library 

kotlin-kapt
👉 Search · kotlin-kapt 

つまり、こういうことでしょうかね。


plugins {
    id 'com.android.application'
-   id 'kotlin-android'
-   id 'android-library'
-   id 'kotlin-kapt'
+   id 'org.jetbrains.kotlin.android'
+   id 'org.jetbrains.kotlin.kapt'

あとは、不要な記述を消し去りたいのですが !

そんなツールを探していたのですが 😅😅😅


【Retorofit】コピペで使える NetworkModule【Dagger Hilt】

もうこれはテンプレ化しておきます。

Retrofit の話が中心となります。

👉 Retrofit 

 

Converter

Retrofit-Converters

JSON 形式のレスポンスをパースして変換するのは、Gson か Moshi が人気のように思います。


var retrofit = Retrofit.Builder()
  .baseUrl("https://api.example.com")
  .addConverterFactory(GsonConverterFactory.create())
  .build()


var retrofit = Retrofit.Builder()
  .baseUrl("https://api.example.com")
  .addConverterFactory(MoshiConverterFactory.create())
  .build()

👉 retrofit/retrofit-converters at master · square/retrofit 

今回は、Kotlin Serialization を利用した「Kotlin Serialization Converter」を使います。

ExperimentalSerializationApi なのですがね。


val contentType = "application/json".toMediaType()
val retrofit = Retrofit.Builder()
    .baseUrl("https://example.com/")
    .addConverterFactory(Json.asConverterFactory(contentType))
    .build()

👉 JakeWharton/retrofit2-kotlinx-serialization-converter: A Retrofit 2 Converter.Factory for Kotlin serialization. 

 

Call Adapter

Retrofit CallAdapters

通信の非同期処理はどれに任せるか。


var retrofit = Retrofit.Builder()
  .baseUrl("https://api.example.com")
  .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
  .build()

👉 retrofit/retrofit-adapters at master · square/retrofit 

Coroutine を使った Jake製の「Kotlin Coroutine Adapter」は 今では DEPRECATED。

👉 JakeWharton/retrofit2-kotlin-coroutines-adapter: A Retrofit 2 adapter for Kotlin coroutine's Deferred type. 

Retrofit は 2.6.0+ で、内部に suspend function をサポートしてるので、特に、.addCallAdapterFactory() を使わなくてもよい。


@GET("users/{id}")
suspend fun user(@Path("id") id: Long): User

👉 retrofit/CHANGELOG.md at master · square/retrofit 

 

HttpLoggingInterceptor

OkHttpClient に HTTP関連のログを吐かせます。


--> POST /greeting http/1.1
Host: example.com
Content-Type: plain/text
Content-Length: 3

Hi?
--> END POST

<-- 200 OK (22ms)
Content-Type: plain/text
Content-Length: 6

Hello!
<-- END HTTP

👉 HttpLoggingInterceptor.Level (OkHttp Logging Interceptor 3.14.0 API) 


val logging = HttpLoggingInterceptor()
logging.setLevel(Level.BASIC)
val client = OkHttpClient.Builder()
  .addInterceptor(logging)
  .build()

👉 okhttp/okhttp-logging-interceptor at master · square/okhttp 

 

まとめ

Dagger の Module にしておきます。

テンプレート化してください、と言わんばかりに

よく似たものをあちこちで見かけますよね。

見通しも良くなります。

コピペでどうぞ。

(おわり)