Slide 1

Slide 1 text

マンガアプリの メモリ改善と解析⽅法 © LINE Digital Frontier Corporation 2022/10/5 Sai.choko

Slide 2

Slide 2 text

• sai.choko • https://twitter.com/zhangho • 中国 -> ⽇本 -> 中国 -> ⽇本 • LINEマンガの開発

Slide 3

Slide 3 text

© LINE Digital Frontier Corporation

Slide 4

Slide 4 text

USER BASE ※2022年9⽉時点 MAU 作品数 取引額 作家数 WEBTOON Entertainment 最新実績 WEBTOON Entertainment が運営する代表的なサービスは 「LINEマンガ (⽇本)」「NAVER WEBTOON (韓国)」「WEBTOON (北南⽶・欧州)」「LINE WEBTOON (東南アジア)」など 8900 万 1000 億円 600 万⼈ 10 億

Slide 5

Slide 5 text

LINEマンガ メモリ問題の背景 © LINE Digital Frontier Corporation

Slide 6

Slide 6 text

レガシー 2013年からアプリを9年以上運⽤したアプリ Java…Kotlin化 AsyncTask…Coroutine アーキテクチャー…MVVMだったり メモリ問題…どこから着⼿すればいいのか︖

Slide 7

Slide 7 text

現在、将来 画⾯回転 Large Screen: Tablet, foldable Google Play Media Experience Program (⼿数料30% -> 15%)

Slide 8

Slide 8 text

マンガViewerはメモリ⼤量消費 縦⻑画像 メイン 画⾯ マンガ Viewer Viewerでは⼗分なメモリを確保したい。

Slide 9

Slide 9 text

本資料の⽬的 • Java/Androidメモリの基礎知識勉強 • メモリ解析ノウハウ共有 • Best practiceをまとめる

Slide 10

Slide 10 text

LINEマンガで Out of memoryが全体crashの⽐率40% -> 5% OOMが40% 他 Crash 他 Crash 他 Crash 他 Crash 他 Crash 他 Crash 他 Crash 他 Crash 他 Crash 他 Crash 他 Crash 他 Crash 他 Crash OOM Crash OOM crash割合<5% 他 Crash 他 Crash 他 Crash 他 Crash 他 Crash 他 Crash 他 Crash 他 Crash 他 Crash 他 Crash 他 Crash OOM Crash こちらはあくまでイメージ図であります。 メモリ改善

Slide 11

Slide 11 text

解析⽅法 & tools • ログ • Java JVM基礎知識 • Android studio メモリprofiler • Android メモリ監視メソッド • Bitmap画像のメモリ計算⽅法

Slide 12

Slide 12 text

12 Crashログ みなさん⼤好きなCrashログ: Crashログから検知 (Firebase Crashlytics、⾃社ログサービスなど) ログだけ頼ってメモリ問題を解決しようとしたが 根本的な解決には繋がらなかった。 いい点: 問題の箇所がすぐわかる 悪い点︓表⾯的な修正に終わる可能性がある、本質的な原因に辿り着かない

Slide 13

Slide 13 text

13 解析⽅法 & tools: ログは最後の段階しか解析できない 量的変化 -> 質的変化 OutOfMemory

Slide 14

Slide 14 text

Java/Android メモリの基礎知識 © LINE Digital Frontier Corporation

Slide 15

Slide 15 text

• JVMのheapに注⽬ Javaメモリ管理⽅法

Slide 16

Slide 16 text

• StackからHeap objectへのReference(参照)ことは線を切ること Java stackとheap

Slide 17

Slide 17 text

• GC Object参照は⾊々絡んでいる。 • Depth: The shortest number of hops from any GC root to the selected instance. GC treeとdepth

Slide 18

Slide 18 text

GC treeとdepth https://www.youtube.com/watch?v=v4kCRZ_O4Lc

Slide 19

Slide 19 text

メモリサイズ https://www.youtube.com/watch?v=v4kCRZ_O4Lc

Slide 20

Slide 20 text

Two important JVM tuning points in the java world • Heap size • -Xms, -Xmx • android:largeHeap="true” • Garbage Collection(GC) • こちらはAndroid OSバージョンとともに進化 https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html - jvms-2.5 https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html Java memory tuning

Slide 21

Slide 21 text

• 特にAndroid 8.0でメモリ管理改善 • Compacting garbage collectorでfragment化したメモリをまとめる • Bitmapはjvm heapではなく、native heapに保存される https://source.android.com/devices/tech/dalvik Android Runtime (ART)

Slide 22

Slide 22 text

OOMの発⽣のデバイス

Slide 23

Slide 23 text

• Android 5,6,7で起こりやすい • RAM 2GB, 3GBで発⽣; 4GB以上は少ない • Android 8以上の端末ではOOMが少ない • ⽐較的にRAMが>=3GBのものが多いのでHeapサイズも⼤きい • Android 8以上ではNative heapにbitmapを保存 • Android 8ではConcurrent compacting garbage collectorなど メモリ回収処理が最適化されている LINEマンガ OOMの発⽣のデバイス統計

Slide 24

Slide 24 text

メモリ解析ツール © LINE Digital Frontier Corporation

Slide 25

Slide 25 text

平坦な下がりグラフにも注意 – メモリGCできていない可能性。 Android studio Profiling – グラフ

Slide 26

Slide 26 text

ALLOCATIONS/NATIVE SIZE/SHALLOW SIZE/RETAINED SIZE / DEPTHの確認(GC ROOTからの距離) Android studio Profiling – グラフ

Slide 27

Slide 27 text

Android Studio profilerの詳しい使い⽅はこちら: https://www.youtube.com/watch?v=v4kCRZ_O4Lc https://developer.android.com/studio/profile/memory- profiler デモ

Slide 28

Slide 28 text

• どんなタイミングでOOM するかを可視化したい • totalFreeMemory が0だ とOOMが発⽣する • 要はVMのmax Memoryを 超えるとメモリが⾜りな いのでOOM https://stackoverflow.com/a/18375641 MemoryUtil

Slide 29

Slide 29 text

totalFreeMemoryで 残りのメモリを数値化したい private const val MEGA_BYTE = 1048576 // 1024 * 1024 @JvmStatic fun printCurrentApplicationVMMemory() { val runtimeVersion = System.getProperty("java.vm.version") val runtime = Runtime.getRuntime() val maxMemory = runtime.maxMemory() / MEGA_BYTE val freeMemory = runtime.freeMemory() / MEGA_BYTE val totalMemory = runtime.totalMemory() / MEGA_BYTE val totalFreeMemory = maxMemory - totalMemory + freeMemory Timber.d( "#Memory CurrentApplicationVMMemory(MB) %s, %s, %s, %s, %s", " totalFreeMemory: $totalFreeMemory", " runtimeVersion: $runtimeVersion", " maxMemory: $maxMemory", " freeMemory: $freeMemory", " totalMemory: $totalMemory", ) } MemoryUtil

Slide 30

Slide 30 text

https://square.github.io/leakcanary/ Leak canary メモリリークの監視ツール

Slide 31

Slide 31 text

具体例 © LINE Digital Frontier Corporation

Slide 32

Slide 32 text

© LINE Digital Frontier Corporation ⼤きい画像はロードしない Do not load large images 画像サイズ調整

Slide 33

Slide 33 text

33 🤔2048 x 1536のjpeg画像のファイルサイズが1MBぐらいなのにAndroid bitmapサイズは12MB になる。なぜ︖ • Bimap画像サイズ(memory) > Jpeg/PNG/webP(圧縮アルゴリズム)ファイルサイズ(dis k) Bitmap画像: ARGB_8888⽅式 (LINEマンガでは画像のクオリティを維持するためRGB_565を採 ⽤しない) で保存する場合のBitmapメモリサイズ(標準保存⽅式) Each pixel is stored on 4 bytes Bitmap image memory size = height x width x 4byte 例: 2048 x 1536 => 2048 x 1536 x 4 = 12582912bytes ≒ 12MB 1024 x 1600 => 1024 x 1600 x 4 = 65536000bytes ≒ 6.8MB 512 x 384 => 512 x 384 x 4 = 786432bytes ≒ 0.75MB • OutOfMemory: Faild to allocate 12582912bytes…の場合は驚かないでください。12MB/4=3MBより ⼩さいファイルサイズ(普段は1MBぐらい)の画像かもしれません。 • 質問: 最新のWebPフォーマットの画像だったらBitmapメモリサイズが⼩さくなる︖ Bitmap画像のメモリサイズ計算⽅法

Slide 34

Slide 34 text

• Picasso .resize(600, 200) • Glideのは基本的にImageViewのサイズを⾃動判別できるが必要に応じ て.override(targetWidth, targetHeight)、 CustomTarget(targetImageWidth, targetImageHeight)でサ イズを設定 画像ライブラリー使う場合: オリジナル画像をロードしない ImageView ImageView サーバーから必要以上に⼤きい画像が送られる場合がありませんか? ImageView サイズ取得

Slide 35

Slide 35 text

• Bitmapを直接弄る時にメモリにロードする時のサイズを調整: • 画像のロードサイズを画⾯サイズに合わせる • option.inSampleSize = 2 // 2の倍数 • https://developer.android.com/topic/performance/graphics/load -bitmap 画像サイズ⼿動調整: inSampleSize

Slide 36

Slide 36 text

画像分割ロード 問題 • 縦⻑い画像のままだと⼆つの問題が発⽣する • GL_MAX_TEXTURE_SIZEとかCanvas: trying to draw too large で古い端末では描画できない可能性がある • RecyclerViewで画⾯に表⽰されていない部分がrecyclerできない 解決⽅法 • meta dataを参考に事前に分割する適切なサイズを計算する • Android 8.0以下: BitmapRegionDecoderで分割読み込み 、 読み込みスピードを犠牲 • Android 8.0以上: オリジナルBitmap読み込んでからsub bitmapに分割して表⽰、 読み込みスピード優先

Slide 37

Slide 37 text

⼀気に多い画像をロードしない Don't load too many images at once 画像ロード量調整

Slide 38

Slide 38 text

マンガViewer、メモリSpike問題

Slide 39

Slide 39 text

• fast scrollで画⾯上に表⽰する必要のない 画像はロードのcallbackが来たら処理しない • NO_POSITIONのholderの画像はもうロードする必要がない。 public void onBitmapLoaded(Bitmap bitmap, … int requestPosition) { …. int adapterPosition = holder. getAbsoluteAdapterPosition(); // check whether it is valid position. // - avoid calling unnecessary logic such as createBitmap. // - this also fix when fast scroll wrong position bitmap set. if (adapterPosition == RecyclerView.NO_POSITION || requestPosition != adapterPosition) { return; } …. Create bitmap … Viewer: RecyclerViewのfast scroll対応 NO_POSITION … … NO_POSITION

Slide 40

Slide 40 text

• low end deviceʼs recyclerview cache limit Viewer: RecyclerViewのviewcache制限

Slide 41

Slide 41 text

無駄なメモリ参照を無くそう もしくは 範囲を狭める Remove useless memory references OR Narrow the scope メモリ参照をなくす

Slide 42

Slide 42 text

• Memcacheがずっとメモリを占⽤しているのを防ぎましょう。 • Picasso⼿動でclearする必要あります。しないとmemcacheで15%のheapメ モリが使えなくなります => Picasso.cache.clear() • Glideなどはactivity/viewなどのcontextにより⾃動メモリ解放しているらしい が必要に応じてclearしておく => Glide.get(context).clearMemory() Memcache: Strong referenceが必要ないときにclearする

Slide 43

Slide 43 text

• addObserver/removeObserve • addListener/removeListener など….. メモリリークはこの系でよく発⽣します。 * 最近のLifecycler系のaddObserverとかはremoveObserverしなくても参照を残さないらしいで す。 参照: addXXXXしたものは必ずremoveXXXX

Slide 44

Slide 44 text

private var binding: xxxxBinding? = null …. override fun onDestroyView() { super.onDestroyView() binding = null } 参照: Release Binding/Adapter

Slide 45

Slide 45 text

• Adapter release when onDestroy • or not use field adapter, just cast for use adapter = null or recyclerView.setAdapter(null); (adapter as xxxxAdapter).get….. 参照: Release Adapter

Slide 46

Slide 46 text

class xxxxFragment { private val myView: View? = null // Bad practice private val myDialogFragment MyDialogFragment? = null // Bad practice } • Filedに保持しない、必要な時はbinding/findFragmentByTag/byId で参照できる 参照: view/dialogを保持しない

Slide 47

Slide 47 text

• Handler(Looper.getMainLooper()).postDelayed(…..). // Bad practice • 実際画像バナー⾃動横scrollでこれを使って無限ループで⾃⼰参照しているところがあ りました。バナー画像は⼤きいですからメモリリークされてメモリにずっと残っていま した。 • rootView.postDelayed(…..) // Good practice • Runnableのstop処理を⾃前で書いてもいいですがこの場合viewのpost/postDelayed が適切 • Viewに紐ついてPostDelayedするとattatch状態もちゃんとチェックしてくれます。 View.java 参照: 遅延処理はViewのpostDelayedを使う

Slide 48

Slide 48 text

広告 sdkなどの実装が悪くでメモリリークが発⽣する可能性 Third party sdk にも注意を払おう

Slide 49

Slide 49 text

https://pszklarska.medium.com/catch-leak-if-you-can-608a99537d8a https://proandroiddev.com/everything-you-need-to-know-about- memory-leaks-in-android-d7a59faaf46a 役に⽴つメモリ関連基礎知識

Slide 50

Slide 50 text

• Flyweight or factory method で⼤きいobjectを使い回す • https://refactoring.guru/design-patterns/flyweight • Singletonもしくはfactory methodでobjectを⼀元管理 おまけ: デザインパターン

Slide 51

Slide 51 text

• Google I/O 2022でもLarge screen(tablet/foldable)が話題 • Large screen対応アプリはGoogle playの該当デバイス種類検索ラン キング上位に現れるなど優遇される • Play Media Experience Programに申請して対応すると⼿数料 が30% -> 15%になります。 • ConfigutationChange/rotationなどで発⽣する可能性が⾼いメモリ リークなど問題ないか解析が必要になっています • LINEマンガは既にLarge Screen対応済み おまけ: Google large screen対応

Slide 52

Slide 52 text

THANK YOU ! Sai.choko © LINE Digital Frontier Corporation